New upstream version 7.1.2

This commit is contained in:
Sandro Tosi 2020-07-21 02:23:42 -04:00
parent 0143a0a452
commit 4e974d1c0d
87 changed files with 5533 additions and 4088 deletions

View file

@ -1,5 +1,95 @@
Click Changelog .. currentmodule:: click
===============
Version 7.1.2
-------------
Released 2020-04-27
- Revert applying shell quoting to commands for ``echo_with_pager``
and ``edit``. This was intended to allows spaces in commands, but
caused issues if the string was actually a command and arguments, or
on Windows. Instead, the string must be quoted manually as it should
appear on the command line. :issue:`1514`
Version 7.1.1
-------------
Released 2020-03-09
- Fix ``ClickException`` output going to stdout instead of stderr.
:issue:`1495`
Version 7.1
-----------
Released 2020-03-09
- Fix PyPI package name, "click" is lowercase again.
- Fix link in ``unicode_literals`` error message. :pr:`1151`
- Add support for colored output on UNIX Jupyter notebooks.
:issue:`1185`
- Operations that strip ANSI controls will strip the cursor hide/show
sequences. :issue:`1216`
- Remove unused compat shim for ``bytes``. :pr:`1195`
- Expand testing around termui, especially getchar on Windows.
:issue:`1116`
- Fix output on Windows Python 2.7 built with MSVC 14. :pr:`1342`
- Fix ``OSError`` when running in MSYS2. :issue:`1338`
- Fix ``OSError`` when redirecting to ``NUL`` stream on Windows.
:issue:`1065`
- Fix memory leak when parsing Unicode arguments on Windows.
:issue:`1136`
- Fix error in new AppEngine environments. :issue:`1462`
- Always return one of the passed choices for ``click.Choice``
:issue:`1277`, :pr:`1318`
- Add ``no_args_is_help`` option to ``click.Command``, defaults to
False :pr:`1167`
- Add ``show_defaults`` parameter to ``Context`` to enable showing
defaults globally. :issue:`1018`
- Handle ``env MYPATH=''`` as though the option were not passed.
:issue:`1196`
- It is once again possible to call ``next(bar)`` on an active
progress bar instance. :issue:`1125`
- ``open_file`` with ``atomic=True`` retains permissions of existing
files and respects the current umask for new files. :issue:`1376`
- When using the test ``CliRunner`` with ``mix_stderr=False``, if
``result.stderr`` is empty it will not raise a ``ValueError``.
:issue:`1193`
- Remove the unused ``mix_stderr`` parameter from
``CliRunner.invoke``. :issue:`1435`
- Fix ``TypeError`` raised when using bool flags and specifying
``type=bool``. :issue:`1287`
- Newlines in option help text are replaced with spaces before
re-wrapping to avoid uneven line breaks. :issue:`834`
- ``MissingParameter`` exceptions are printable in the Python
interpreter. :issue:`1139`
- Fix how default values for file-type options are shown during
prompts. :issue:`914`
- Fix environment variable automatic generation for commands
containing ``-``. :issue:`1253`
- Option help text replaces newlines with spaces when rewrapping, but
preserves paragraph breaks, fixing multiline formatting.
:issue:`834, 1066, 1397`
- Option help text that is wrapped adds an extra newline at the end to
distinguish it from the next option. :issue:`1075`
- Consider ``sensible-editor`` when determining the editor to use for
``click.edit()``. :pr:`1469`
- Arguments to system calls such as the executable path passed to
``click.edit`` can contains spaces. :pr:`1470`
- Add ZSH completion autoloading and error handling. :issue:`1348`
- Add a repr to ``Command``, ``Group``, ``Option``, and ``Argument``,
showing the name for friendlier debugging. :issue:`1267`
- Completion doesn't consider option names if a value starts with
``-`` after the ``--`` separator. :issue:`1247`
- ZSH completion escapes special characters in values. :pr:`1418`
- Add completion support for Fish shell. :pr:`1423`
- Decoding bytes option values falls back to UTF-8 in more cases.
:pr:`1468`
- Make the warning about old 2-arg parameter callbacks a deprecation
warning, to be removed in 8.0. This has been a warning since Click
2.0. :pr:`1492`
Version 7.0 Version 7.0
@ -7,629 +97,540 @@ Version 7.0
Released 2018-09-25 Released 2018-09-25
- Drop support for Python 2.6 and 3.3. (`#967`_, `#976`_) - Drop support for Python 2.6 and 3.3. :pr:`967, 976`
- Wrap ``click.Choice``'s missing message. (`#202`_, `#1000`_) - Wrap ``click.Choice``'s missing message. :issue:`202`, :pr:`1000`
- Add native ZSH autocompletion support. (`#323`_, `#865`_) - Add native ZSH autocompletion support. :issue:`323`, :pr:`865`
- Document that ANSI color info isn't parsed from bytearrays in - Document that ANSI color info isn't parsed from bytearrays in Python
Python 2. (`#334`_) 2. :issue:`334`
- Document byte-stripping behavior of ``CliRunner``. (`#334`_, - Document byte-stripping behavior of ``CliRunner``. :issue:`334`,
`#1010`_) :pr:`1010`
- Usage errors now hint at the ``--help`` option. (`#393`_, `#557`_) - Usage errors now hint at the ``--help`` option. :issue:`393`,
- Implement streaming pager. (`#409`_, `#889`_) :pr:`557`
- Extract bar formatting to its own method. (`#414`_) - Implement streaming pager. :issue:`409`, :pr:`889`
- Extract bar formatting to its own method. :pr:`414`
- Add ``DateTime`` type for converting input in given date time - Add ``DateTime`` type for converting input in given date time
formats. (`#423`_) formats. :pr:`423`
- ``secho``'s first argument can now be ``None``, like in ``echo``. - ``secho``'s first argument can now be ``None``, like in ``echo``.
(`#424`_) :pr:`424`
- Fixes a ``ZeroDivisionError`` in ``ProgressBar.make_step``, when the - Fixes a ``ZeroDivisionError`` in ``ProgressBar.make_step``, when the
arg passed to the first call of ``ProgressBar.update`` is 0. arg passed to the first call of ``ProgressBar.update`` is 0.
(`#447`_, `#1012`_) :issue:`447`, :pr:`1012`
- Show progressbar only if total execution time is visible. (`#487`_) - Show progressbar only if total execution time is visible. :pr:`487`
- Added the ability to hide commands and options from help. (`#500`_) - Added the ability to hide commands and options from help. :pr:`500`
- Document that options can be ``required=True``. (`#514`_, `#1022`_) - Document that options can be ``required=True``. :issue:`514`,
:pr:`1022`
- Non-standalone calls to ``Context.exit`` return the exit code, - Non-standalone calls to ``Context.exit`` return the exit code,
rather than calling ``sys.exit``. (`#533`_, `#667`_, `#1098`_) rather than calling ``sys.exit``. :issue:`667`, :pr:`533, 1098`
- ``click.getchar()`` returns Unicode in Python 3 on Windows, - ``click.getchar()`` returns Unicode in Python 3 on Windows,
consistent with other platforms. (`#537`_, `#821`_, `#822`_, consistent with other platforms. :issue:`537, 821, 822, 1088`,
`#1088`_, `#1108`_) :pr:`1108`
- Added ``FloatRange`` type. (`#538`_, `#553`_) - Added ``FloatRange`` type. :pr:`538, 553`
- Added support for bash completion of ``type=click.Choice`` for - Added support for bash completion of ``type=click.Choice`` for
``Options`` and ``Arguments``. (`#535`_, `#681`_) ``Options`` and ``Arguments``. :issue:`535`, :pr:`681`
- Only allow one positional arg for ``Argument`` parameter - Only allow one positional arg for ``Argument`` parameter
declaration. (`#568`_, `#574`_, `#1014`_) declaration. :issue:`568, 574`, :pr:`1014`
- Add ``case_sensitive=False`` as an option to Choice. (`#569`_) - Add ``case_sensitive=False`` as an option to Choice. :issue:`569`
- ``click.getchar()`` correctly raises ``KeyboardInterrupt`` on "^C" - ``click.getchar()`` correctly raises ``KeyboardInterrupt`` on "^C"
and ``EOFError`` on "^D" on Linux. (`#583`_, `#1115`_) and ``EOFError`` on "^D" on Linux. :issue:`583`, :pr:`1115`
- Fix encoding issue with ``click.getchar(echo=True)`` on Linux. - Fix encoding issue with ``click.getchar(echo=True)`` on Linux.
(`#1115`_) :pr:`1115`
- ``param_hint`` in errors now derived from param itself. (`#598`_, - ``param_hint`` in errors now derived from param itself.
`#704`_, `#709`_) :issue:`598, 704`, :pr:`709`
- Add a test that ensures that when an argument is formatted into a - Add a test that ensures that when an argument is formatted into a
usage error, its metavar is used, not its name. (`#612`_) usage error, its metavar is used, not its name. :pr:`612`
- Allow setting ``prog_name`` as extra in ``CliRunner.invoke``. - Allow setting ``prog_name`` as extra in ``CliRunner.invoke``.
(`#616`_, `#999`_) :issue:`616`, :pr:`999`
- Help text taken from docstrings truncates at the ``\f`` form feed - Help text taken from docstrings truncates at the ``\f`` form feed
character, useful for hiding Sphinx-style parameter documentation. character, useful for hiding Sphinx-style parameter documentation.
(`#629`_, `#1091`_) :pr:`629, 1091`
- ``launch`` now works properly under Cygwin. (`#650`_) - ``launch`` now works properly under Cygwin. :pr:`650`
- Update progress after iteration. (`#651`_, `#706`_) - Update progress after iteration. :issue:`651`, :pr:`706`
- ``CliRunner.invoke`` now may receive ``args`` as a string - ``CliRunner.invoke`` now may receive ``args`` as a string
representing a Unix shell command. (`#664`_) representing a Unix shell command. :pr:`664`
- Make ``Argument.make_metavar()`` default to type metavar. (`#675`_) - Make ``Argument.make_metavar()`` default to type metavar. :pr:`675`
- Add documentation for ``ignore_unknown_options``. (`#684`_) - Add documentation for ``ignore_unknown_options``. :pr:`684`
- Add bright colors support for ``click.style`` and fix the reset - Add bright colors support for ``click.style`` and fix the reset
option for parameters ``fg`` and ``bg``. (`#703`_, `#809`_) option for parameters ``fg`` and ``bg``. :issue:`703`, :pr:`809`
- Add ``show_envvar`` for showing environment variables in help. - Add ``show_envvar`` for showing environment variables in help.
(`#710`_) :pr:`710`
- Avoid ``BrokenPipeError`` during interpreter shutdown when stdout or - Avoid ``BrokenPipeError`` during interpreter shutdown when stdout or
stderr is a closed pipe. (`#712`_, `#1106`_) stderr is a closed pipe. :issue:`712`, :pr:`1106`
- Document customizing option names. (`#725`_, `#1016`_) - Document customizing option names. :issue:`725`, :pr:`1016`
- Disable ``sys._getframes()`` on Python interpreters that don't - Disable ``sys._getframes()`` on Python interpreters that don't
support it. (`#728`_) support it. :pr:`728`
- Fix bug in test runner when calling ``sys.exit`` with ``None``. - Fix bug in test runner when calling ``sys.exit`` with ``None``.
(`#739`_) :pr:`739`
- Clarify documentation on command line options. (`#741`_, `#1003`_) - Clarify documentation on command line options. :issue:`741`,
- Fix crash on Windows console. (`#744`_) :pr:`1003`
- Fix crash on Windows console. :issue:`744`
- Fix bug that caused bash completion to give improper completions on - Fix bug that caused bash completion to give improper completions on
chained commands. (`#754`_, `#774`_) chained commands. :issue:`754`, :pr:`774`
- Added support for dynamic bash completion from a user-supplied - Added support for dynamic bash completion from a user-supplied
callback. (`#755`_) callback. :pr:`755`
- Added support for bash completions containing spaces. (`#773`_) - Added support for bash completions containing spaces. :pr:`773`
- Allow autocompletion function to determine whether or not to return - Allow autocompletion function to determine whether or not to return
completions that start with the incomplete argument. (`#790`_, completions that start with the incomplete argument. :issue:`790`,
`#806`_) :pr:`806`
- Fix option naming routine to match documentation and be - Fix option naming routine to match documentation and be
deterministic. (`#793`_, `#794`_) deterministic. :issue:`793`, :pr:`794`
- Fix path validation bug. (`#795`_, `#1020`_) - Fix path validation bug. :issue:`795`, :pr:`1020`
- Add test and documentation for ``Option`` naming: functionality. - Add test and documentation for ``Option`` naming: functionality.
(`#799`_) :pr:`799`
- Update doc to match arg name for ``path_type``. (`#801`_) - Update doc to match arg name for ``path_type``. :pr:`801`
- Raw strings added so correct escaping occurs. (`#807`_) - Raw strings added so correct escaping occurs. :pr:`807`
- Fix 16k character limit of ``click.echo`` on Windows. (`#816`_, - Fix 16k character limit of ``click.echo`` on Windows. :issue:`816`,
`#819`_) :pr:`819`
- Overcome 64k character limit when writing to binary stream on - Overcome 64k character limit when writing to binary stream on
Windows 7. (`#825`_, `#830`_) Windows 7. :issue:`825`, :pr:`830`
- Add bool conversion for "t" and "f". (`#842`_) - Add bool conversion for "t" and "f". :pr:`842`
- ``NoSuchOption`` errors take ``ctx`` so that ``--help`` hint gets - ``NoSuchOption`` errors take ``ctx`` so that ``--help`` hint gets
printed in error output. (`#860`_) printed in error output. :pr:`860`
- Fixed the behavior of Click error messages with regards to Unicode - Fixed the behavior of Click error messages with regards to Unicode
on 2.x and 3.x. Message is now always Unicode and the str and on 2.x and 3.x. Message is now always Unicode and the str and
Unicode special methods work as you expect on that platform. Unicode special methods work as you expect on that platform.
(`#862`_) :issue:`862`
- Progress bar now uses stderr by default. (`#863`_) - Progress bar now uses stderr by default. :pr:`863`
- Add support for auto-completion documentation. (`#866`_, `#869`_) - Add support for auto-completion documentation. :issue:`866`,
- Allow ``CliRunner`` to separate stdout and stderr. (`#868`_) :pr:`869`
- Fix variable precedence. (`#873`_, `#874`_) - Allow ``CliRunner`` to separate stdout and stderr. :pr:`868`
- Fix invalid escape sequences. (`#877`_) - Fix variable precedence. :issue:`873`, :pr:`874`
- Fix ``ResourceWarning`` that occurs during some tests. (`#878`_) - Fix invalid escape sequences. :pr:`877`
- Fix ``ResourceWarning`` that occurs during some tests. :pr:`878`
- When detecting a misconfigured locale, don't fail if the ``locale`` - When detecting a misconfigured locale, don't fail if the ``locale``
command fails. (`#880`_) command fails. :pr:`880`
- Add ``case_sensitive=False`` as an option to ``Choice`` types. - Add ``case_sensitive=False`` as an option to ``Choice`` types.
(`#887`_) :pr:`887`
- Force stdout/stderr writable. This works around issues with badly - Force stdout/stderr writable. This works around issues with badly
patched standard streams like those from Jupyter. (`#918`_) patched standard streams like those from Jupyter. :pr:`918`
- Fix completion of subcommand options after last argument (`#919`_, - Fix completion of subcommand options after last argument
`#930`_) :issue:`919`, :pr:`930`
- ``_AtomicFile`` now uses the ``realpath`` of the original filename - ``_AtomicFile`` now uses the ``realpath`` of the original filename
so that changing the working directory does not affect it. so that changing the working directory does not affect it. :pr:`920`
(`#920`_) - Fix incorrect completions when defaults are present :issue:`925`,
- Fix incorrect completions when defaults are present (`#925`_, :pr:`930`
`#930`_)
- Add copy option attrs so that custom classes can be re-used. - Add copy option attrs so that custom classes can be re-used.
(`#926`_, `#994`_) :issue:`926`, :pr:`994`
- "x" and "a" file modes now use stdout when file is ``"-"``. - "x" and "a" file modes now use stdout when file is ``"-"``.
(`#929`_) :pr:`929`
- Fix missing comma in ``__all__`` list. (`#935`_) - Fix missing comma in ``__all__`` list. :pr:`935`
- Clarify how parameters are named. (`#949`_, `#1009`_) - Clarify how parameters are named. :issue:`949`, :pr:`1009`
- Stdout is now automatically set to non blocking. (`#954`_) - Stdout is now automatically set to non blocking. :pr:`954`
- Do not set options twice. (`#962`_) - Do not set options twice. :pr:`962`
- Move ``fcntl`` import. (`#965`_) - Move ``fcntl`` import. :pr:`965`
- Fix Google App Engine ``ImportError``. (`#995`_) - Fix Google App Engine ``ImportError``. :pr:`995`
- Better handling of help text for dynamic default option values. - Better handling of help text for dynamic default option values.
(`#996`_) :pr:`996`
- Fix ``get_winter_size()`` so it correctly returns ``(0,0)``. - Fix ``get_winter_size()`` so it correctly returns ``(0,0)``.
(`#997`_) :pr:`997`
- Add test case checking for custom param type. (`#1001`_) - Add test case checking for custom param type. :pr:`1001`
- Allow short width to address cmd formatting. (`#1002`_) - Allow short width to address cmd formatting. :pr:`1002`
- Add details about Python version support. (`#1004`_) - Add details about Python version support. :pr:`1004`
- Added deprecation flag to commands. (`#1005`_) - Added deprecation flag to commands. :pr:`1005`
- Fixed issues where ``fd`` was undefined. (`#1007`_) - Fixed issues where ``fd`` was undefined. :pr:`1007`
- Fix formatting for short help. (`#1008`_) - Fix formatting for short help. :pr:`1008`
- Document how ``auto_envvar_prefix`` works with command groups. - Document how ``auto_envvar_prefix`` works with command groups.
(`#1011`_) :pr:`1011`
- Don't add newlines by default for progress bars. (`#1013`_) - Don't add newlines by default for progress bars. :pr:`1013`
- Use Python sorting order for ZSH completions. (`#1047`_, `#1059`_) - Use Python sorting order for ZSH completions. :issue:`1047`,
:pr:`1059`
- Document that parameter names are converted to lowercase by default. - Document that parameter names are converted to lowercase by default.
(`#1055`_) :pr:`1055`
- Subcommands that are named by the function now automatically have - Subcommands that are named by the function now automatically have
the underscore replaced with a dash. If you register a function the underscore replaced with a dash. If you register a function
named ``my_command`` it becomes ``my-command`` in the command line named ``my_command`` it becomes ``my-command`` in the command line
interface. interface.
- Hide hidden commands and options from completion. (`#1058`_, - Hide hidden commands and options from completion. :issue:`1058`,
`#1061`_) :pr:`1061`
- Fix absolute import blocking Click from being vendored into a - Fix absolute import blocking Click from being vendored into a
project on Windows. (`#1068`_, `#1069`_) project on Windows. :issue:`1068`, :pr:`1069`
- Fix issue where a lowercase ``auto_envvar_prefix`` would not be - Fix issue where a lowercase ``auto_envvar_prefix`` would not be
converted to uppercase. (`#1105`_) converted to uppercase. :pr:`1105`
.. _#202: https://github.com/pallets/click/issues/202
.. _#323: https://github.com/pallets/click/issues/323
.. _#334: https://github.com/pallets/click/issues/334
.. _#393: https://github.com/pallets/click/issues/393
.. _#409: https://github.com/pallets/click/issues/409
.. _#414: https://github.com/pallets/click/pull/414
.. _#423: https://github.com/pallets/click/pull/423
.. _#424: https://github.com/pallets/click/pull/424
.. _#447: https://github.com/pallets/click/issues/447
.. _#487: https://github.com/pallets/click/pull/487
.. _#500: https://github.com/pallets/click/pull/500
.. _#514: https://github.com/pallets/click/issues/514
.. _#533: https://github.com/pallets/click/pull/533
.. _#535: https://github.com/pallets/click/issues/535
.. _#537: https://github.com/pallets/click/issues/537
.. _#538: https://github.com/pallets/click/pull/538
.. _#553: https://github.com/pallets/click/pull/553
.. _#557: https://github.com/pallets/click/pull/557
.. _#568: https://github.com/pallets/click/issues/568
.. _#569: https://github.com/pallets/click/issues/569
.. _#574: https://github.com/pallets/click/issues/574
.. _#583: https://github.com/pallets/click/issues/583
.. _#598: https://github.com/pallets/click/issues/598
.. _#612: https://github.com/pallets/click/pull/612
.. _#616: https://github.com/pallets/click/issues/616
.. _#629: https://github.com/pallets/click/pull/629
.. _#650: https://github.com/pallets/click/pull/650
.. _#651: https://github.com/pallets/click/issues/651
.. _#664: https://github.com/pallets/click/pull/664
.. _#667: https://github.com/pallets/click/issues/667
.. _#675: https://github.com/pallets/click/pull/675
.. _#681: https://github.com/pallets/click/pull/681
.. _#684: https://github.com/pallets/click/pull/684
.. _#703: https://github.com/pallets/click/issues/703
.. _#704: https://github.com/pallets/click/issues/704
.. _#706: https://github.com/pallets/click/pull/706
.. _#709: https://github.com/pallets/click/pull/709
.. _#710: https://github.com/pallets/click/pull/710
.. _#712: https://github.com/pallets/click/pull/712
.. _#719: https://github.com/pallets/click/issues/719
.. _#725: https://github.com/pallets/click/issues/725
.. _#728: https://github.com/pallets/click/pull/728
.. _#739: https://github.com/pallets/click/pull/739
.. _#741: https://github.com/pallets/click/issues/741
.. _#744: https://github.com/pallets/click/issues/744
.. _#754: https://github.com/pallets/click/issues/754
.. _#755: https://github.com/pallets/click/pull/755
.. _#773: https://github.com/pallets/click/pull/773
.. _#774: https://github.com/pallets/click/pull/774
.. _#790: https://github.com/pallets/click/issues/790
.. _#793: https://github.com/pallets/click/issues/793
.. _#794: https://github.com/pallets/click/pull/794
.. _#795: https://github.com/pallets/click/issues/795
.. _#799: https://github.com/pallets/click/pull/799
.. _#801: https://github.com/pallets/click/pull/801
.. _#806: https://github.com/pallets/click/pull/806
.. _#807: https://github.com/pallets/click/pull/807
.. _#809: https://github.com/pallets/click/pull/809
.. _#816: https://github.com/pallets/click/pull/816
.. _#819: https://github.com/pallets/click/pull/819
.. _#821: https://github.com/pallets/click/issues/821
.. _#822: https://github.com/pallets/click/issues/822
.. _#825: https://github.com/pallets/click/issues/825
.. _#830: https://github.com/pallets/click/pull/830
.. _#842: https://github.com/pallets/click/pull/842
.. _#860: https://github.com/pallets/click/issues/860
.. _#862: https://github.com/pallets/click/issues/862
.. _#863: https://github.com/pallets/click/pull/863
.. _#865: https://github.com/pallets/click/pull/865
.. _#866: https://github.com/pallets/click/issues/866
.. _#868: https://github.com/pallets/click/pull/868
.. _#869: https://github.com/pallets/click/pull/869
.. _#873: https://github.com/pallets/click/issues/873
.. _#874: https://github.com/pallets/click/pull/874
.. _#877: https://github.com/pallets/click/pull/877
.. _#878: https://github.com/pallets/click/pull/878
.. _#880: https://github.com/pallets/click/pull/880
.. _#883: https://github.com/pallets/click/pull/883
.. _#887: https://github.com/pallets/click/pull/887
.. _#889: https://github.com/pallets/click/pull/889
.. _#918: https://github.com/pallets/click/pull/918
.. _#919: https://github.com/pallets/click/issues/919
.. _#920: https://github.com/pallets/click/pull/920
.. _#925: https://github.com/pallets/click/issues/925
.. _#926: https://github.com/pallets/click/issues/926
.. _#929: https://github.com/pallets/click/pull/929
.. _#930: https://github.com/pallets/click/pull/930
.. _#935: https://github.com/pallets/click/pull/935
.. _#949: https://github.com/pallets/click/issues/949
.. _#954: https://github.com/pallets/click/pull/954
.. _#962: https://github.com/pallets/click/pull/962
.. _#965: https://github.com/pallets/click/pull/965
.. _#967: https://github.com/pallets/click/pull/967
.. _#976: https://github.com/pallets/click/pull/976
.. _#990: https://github.com/pallets/click/pull/990
.. _#991: https://github.com/pallets/click/pull/991
.. _#993: https://github.com/pallets/click/pull/993
.. _#994: https://github.com/pallets/click/pull/994
.. _#995: https://github.com/pallets/click/pull/995
.. _#996: https://github.com/pallets/click/pull/996
.. _#997: https://github.com/pallets/click/pull/997
.. _#999: https://github.com/pallets/click/pull/999
.. _#1000: https://github.com/pallets/click/pull/1000
.. _#1001: https://github.com/pallets/click/pull/1001
.. _#1002: https://github.com/pallets/click/pull/1002
.. _#1003: https://github.com/pallets/click/pull/1003
.. _#1004: https://github.com/pallets/click/pull/1004
.. _#1005: https://github.com/pallets/click/pull/1005
.. _#1007: https://github.com/pallets/click/pull/1007
.. _#1008: https://github.com/pallets/click/pull/1008
.. _#1009: https://github.com/pallets/click/pull/1009
.. _#1010: https://github.com/pallets/click/pull/1010
.. _#1011: https://github.com/pallets/click/pull/1011
.. _#1012: https://github.com/pallets/click/pull/1012
.. _#1013: https://github.com/pallets/click/pull/1013
.. _#1014: https://github.com/pallets/click/pull/1014
.. _#1016: https://github.com/pallets/click/pull/1016
.. _#1020: https://github.com/pallets/click/pull/1020
.. _#1022: https://github.com/pallets/click/pull/1022
.. _#1027: https://github.com/pallets/click/pull/1027
.. _#1047: https://github.com/pallets/click/pull/1047
.. _#1055: https://github.com/pallets/click/pull/1055
.. _#1058: https://github.com/pallets/click/pull/1058
.. _#1059: https://github.com/pallets/click/pull/1059
.. _#1061: https://github.com/pallets/click/pull/1061
.. _#1068: https://github.com/pallets/click/issues/1068
.. _#1069: https://github.com/pallets/click/pull/1069
.. _#1088: https://github.com/pallets/click/issues/1088
.. _#1091: https://github.com/pallets/click/pull/1091
.. _#1098: https://github.com/pallets/click/pull/1098
.. _#1105: https://github.com/pallets/click/pull/1105
.. _#1106: https://github.com/pallets/click/pull/1106
.. _#1108: https://github.com/pallets/click/pull/1108
.. _#1115: https://github.com/pallets/click/pull/1115
Version 6.7 Version 6.7
----------- -----------
(bugfix release; released on January 6th 2017) Released 2017-01-06
- Make ``click.progressbar`` work with ``codecs.open`` files.
:pr:`637`
- Fix bug in bash completion with nested subcommands. :pr:`639`
- Fix test runner not saving caller env correctly. :pr:`644`
- Fix handling of SIGPIPE. :pr:`62`
- Deal with broken Windows environments such as Google App Engine's.
:issue:`711`
- Make ``click.progressbar`` work with ``codecs.open`` files. See #637.
- Fix bug in bash completion with nested subcommands. See #639.
- Fix test runner not saving caller env correctly. See #644.
- Fix handling of SIGPIPE. See #626
- Deal with broken Windows environments such as Google App Engine's. See #711.
Version 6.6 Version 6.6
----------- -----------
(bugfix release; released on April 4th 2016) Released 2016-04-04
- Fix bug in ``click.Path`` where it would crash when passed a ``-``.
:issue:`551`
- Fix bug in ``click.Path`` where it would crash when passed a ``-``. See #551.
Version 6.4 Version 6.4
----------- -----------
(bugfix release; released on March 24th 2016) Released 2016-03-24
- Fix bug in bash completion where click would discard one or more
trailing arguments. :issue:`471`
- Fix bug in bash completion where click would discard one or more trailing
arguments. See #471.
Version 6.3 Version 6.3
----------- -----------
(bugfix release; released on February 22 2016) Released 2016-02-22
- Fix argument checks for interpreter invoke with ``-m`` and ``-c`` on
Windows.
- Fixed a bug that cased locale detection to error out on Python 3.
- Fix argument checks for interpreter invoke with ``-m`` and ``-c``
on Windows.
- Fixed a bug that cased locale detection to error out on Python 3.
Version 6.2 Version 6.2
----------- -----------
(bugfix release, released on November 27th 2015) Released 2015-11-27
- Correct fix for hidden progress bars.
- Correct fix for hidden progress bars.
Version 6.1 Version 6.1
----------- -----------
(bugfix release, released on November 27th 2015) Released 2015-11-27
- Resolved an issue with invisible progress bars no longer rendering.
- Disable chain commands with subcommands as they were inherently
broken.
- Fix ``MissingParameter`` not working without parameters passed.
- Resolved an issue with invisible progress bars no longer rendering.
- Disable chain commands with subcommands as they were inherently broken.
- Fix ``MissingParameter`` not working without parameters passed.
Version 6.0 Version 6.0
----------- -----------
(codename "pow pow", released on November 24th 2015) Released 2015-11-24, codename "pow pow"
- Optimized the progressbar rendering to not render when it did not
actually change.
- Explicitly disallow ``nargs=-1`` with a set default.
- The context is now closed before it's popped from the stack.
- Added support for short aliases for the false flag on toggles.
- Click will now attempt to aid you with debugging locale errors
better by listing with the help of the OS what locales are
available.
- Click used to return byte strings on Python 2 in some unit-testing
situations. This has been fixed to correctly return unicode strings
now.
- For Windows users on Python 2, Click will now handle Unicode more
correctly handle Unicode coming in from the system. This also has
the disappointing side effect that filenames will now be always
unicode by default in the ``Path`` type which means that this can
introduce small bugs for code not aware of this.
- Added a ``type`` parameter to ``Path`` to force a specific string
type on the value.
- For users running Python on Windows the ``echo`` and ``prompt``
functions now work with full unicode functionality in the Python
windows console by emulating an output stream. This also applies to
getting the virtual output and input streams via
``click.get_text_stream(...)``.
- Unittests now always force a certain virtual terminal width.
- Added support for allowing dashes to indicate standard streams to
the ``Path`` type.
- Multi commands in chain mode no longer propagate arguments left over
from parsing to the callbacks. It's also now disallowed through an
exception when optional arguments are attached to multi commands if
chain mode is enabled.
- Relaxed restriction that disallowed chained commands to have other
chained commands as child commands.
- Arguments with positive nargs can now have defaults implemented.
Previously this configuration would often result in slightly
unexpected values be returned.
- Optimized the progressbar rendering to not render when it did not
actually change.
- Explicitly disallow ``nargs=-1`` with a set default.
- The context is now closed before it's popped from the stack.
- Added support for short aliases for the false flag on toggles.
- Click will now attempt to aid you with debugging locale errors
better by listing with the help of the OS what locales are
available.
- Click used to return byte strings on Python 2 in some unit-testing
situations. This has been fixed to correctly return unicode strings
now.
- For Windows users on Python 2, Click will now handle Unicode more
correctly handle Unicode coming in from the system. This also has
the disappointing side effect that filenames will now be always
unicode by default in the ``Path`` type which means that this can
introduce small bugs for code not aware of this.
- Added a ``type`` parameter to ``Path`` to force a specific string type
on the value.
- For users running Python on Windows the ``echo`` and ``prompt`` functions
now work with full unicode functionality in the Python windows console
by emulating an output stream. This also applies to getting the
virtual output and input streams via ``click.get_text_stream(...)``.
- Unittests now always force a certain virtual terminal width.
- Added support for allowing dashes to indicate standard streams to the
``Path`` type.
- Multi commands in chain mode no longer propagate arguments left over
from parsing to the callbacks. It's also now disallowed through an
exception when optional arguments are attached to multi commands if chain
mode is enabled.
- Relaxed restriction that disallowed chained commands to have other
chained commands as child commands.
- Arguments with positive nargs can now have defaults implemented.
Previously this configuration would often result in slightly unexpected
values be returned.
Version 5.1 Version 5.1
----------- -----------
(bugfix release, released on 17th August 2015) Released 2015-08-17
- Fix a bug in ``pass_obj`` that would accidentally pass the context
too.
- Fix a bug in ``pass_obj`` that would accidentally pass the context too.
Version 5.0 Version 5.0
----------- -----------
(codename "tok tok", released on 16th August 2015) Released 2015-08-16, codename "tok tok"
- Removed various deprecated functionality.
- Atomic files now only accept the ``w`` mode.
- Change the usage part of help output for very long commands to wrap
their arguments onto the next line, indented by 4 spaces.
- Fix a bug where return code and error messages were incorrect when
using ``CliRunner``.
- Added ``get_current_context``.
- Added a ``meta`` dictionary to the context which is shared across
the linked list of contexts to allow click utilities to place state
there.
- Introduced ``Context.scope``.
- The ``echo`` function is now threadsafe: It calls the ``write``
method of the underlying object only once.
- ``prompt(hide_input=True)`` now prints a newline on ``^C``.
- Click will now warn if users are using ``unicode_literals``.
- Click will now ignore the ``PAGER`` environment variable if it is
empty or contains only whitespace.
- The ``click-contrib`` GitHub organization was created.
- Removed various deprecated functionality.
- Atomic files now only accept the ``w`` mode.
- Change the usage part of help output for very long commands to wrap
their arguments onto the next line, indented by 4 spaces.
- Fix a bug where return code and error messages were incorrect when
using ``CliRunner``.
- added ``get_current_context``.
- added a ``meta`` dictionary to the context which is shared across the
linked list of contexts to allow click utilities to place state there.
- introduced ``Context.scope``.
- The ``echo`` function is now threadsafe: It calls the ``write`` method of the
underlying object only once.
- ``prompt(hide_input=True)`` now prints a newline on ``^C``.
- Click will now warn if users are using ``unicode_literals``.
- Click will now ignore the ``PAGER`` environment variable if it is empty or
contains only whitespace.
- The ``click-contrib`` GitHub organization was created.
Version 4.1 Version 4.1
----------- -----------
(bugfix release, released on July 14th 2015) Released 2015-07-14
- Fix a bug where error messages would include a trailing ``None``
string.
- Fix a bug where Click would crash on docstrings with trailing
newlines.
- Support streams with encoding set to ``None`` on Python 3 by barfing
with a better error.
- Handle ^C in less-pager properly.
- Handle return value of ``None`` from ``sys.getfilesystemencoding``
- Fix crash when writing to unicode files with ``click.echo``.
- Fix type inference with multiple options.
- Fix a bug where error messages would include a trailing ``None`` string.
- Fix a bug where Click would crash on docstrings with trailing newlines.
- Support streams with encoding set to ``None`` on Python 3 by barfing with
a better error.
- Handle ^C in less-pager properly.
- Handle return value of ``None`` from ``sys.getfilesystemencoding``
- Fix crash when writing to unicode files with ``click.echo``.
- Fix type inference with multiple options.
Version 4.0 Version 4.0
----------- -----------
(codename "zoom zoom", released on March 31st 2015) Released 2015-03-31, codename "zoom zoom"
- Added ``color`` parameters to lots of interfaces that directly or
indirectly call into echoing. This previously was always
autodetection (with the exception of the ``echo_via_pager``
function). Now you can forcefully enable or disable it, overriding
the auto detection of Click.
- Added an ``UNPROCESSED`` type which does not perform any type
changes which simplifies text handling on 2.x / 3.x in some special
advanced usecases.
- Added ``NoSuchOption`` and ``BadOptionUsage`` exceptions for more
generic handling of errors.
- Added support for handling of unprocessed options which can be
useful in situations where arguments are forwarded to underlying
tools.
- Added ``max_content_width`` parameter to the context which can be
used to change the maximum width of help output. By default Click
will not format content for more than 80 characters width.
- Added support for writing prompts to stderr.
- Fix a bug when showing the default for multiple arguments.
- Added support for custom subclasses to ``option`` and ``argument``.
- Fix bug in ``clear()`` on Windows when colorama is installed.
- Reject ``nargs=-1`` for options properly. Options cannot be
variadic.
- Fixed an issue with bash completion not working properly for
commands with non ASCII characters or dashes.
- Added a way to manually update the progressbar.
- Changed the formatting of missing arguments. Previously the internal
argument name was shown in error messages, now the metavar is shown
if passed. In case an automated metavar is selected, it's stripped
of extra formatting first.
- Added ``color`` parameters to lots of interfaces that directly or indirectly
call into echoing. This previously was always autodetection (with the
exception of the ``echo_via_pager`` function). Now you can forcefully
enable or disable it, overriding the auto detection of Click.
- Added an ``UNPROCESSED`` type which does not perform any type changes which
simplifies text handling on 2.x / 3.x in some special advanced usecases.
- Added ``NoSuchOption`` and ``BadOptionUsage`` exceptions for more generic
handling of errors.
- Added support for handling of unprocessed options which can be useful in
situations where arguments are forwarded to underlying tools.
- Added ``max_content_width`` parameter to the context which can be used to
change the maximum width of help output. By default Click will not format
content for more than 80 characters width.
- Added support for writing prompts to stderr.
- Fix a bug when showing the default for multiple arguments.
- Added support for custom subclasses to ``option`` and ``argument``.
- Fix bug in ``clear()`` on Windows when colorama is installed.
- Reject ``nargs=-1`` for options properly. Options cannot be variadic.
- Fixed an issue with bash completion not working properly for commands with
non ASCII characters or dashes.
- Added a way to manually update the progressbar.
- Changed the formatting of missing arguments. Previously the internal
argument name was shown in error messages, now the metavar is shown if
passed. In case an automated metavar is selected, it's stripped of
extra formatting first.
Version 3.3 Version 3.3
----------- -----------
(bugfix release, released on September 8th 2014) Released 2014-09-08
- Fixed an issue with error reporting on Python 3 for invalid
forwarding of commands.
- Fixed an issue with error reporting on Python 3 for invalid forwarding
of commands.
Version 3.2 Version 3.2
----------- -----------
(bugfix release, released on August 22nd 2014) Released 2014-08-22
- Added missing ``err`` parameter forwarding to the ``secho``
function.
- Fixed default parameters not being handled properly by the context
invoke method. This is a backwards incompatible change if the
function was used improperly. See :ref:`upgrade-to-3.2` for more
information.
- Removed the ``invoked_subcommands`` attribute largely. It is not
possible to provide it to work error free due to how the parsing
works so this API has been deprecated. See :ref:`upgrade-to-3.2` for
more information.
- Restored the functionality of ``invoked_subcommand`` which was
broken as a regression in 3.1.
- Added missing ``err`` parameter forwarding to the ``secho`` function.
- Fixed default parameters not being handled properly by the context
invoke method. This is a backwards incompatible change if the function
was used improperly. See :ref:`upgrade-to-3.2` for more information.
- Removed the `invoked_subcommands` attribute largely. It is not possible
to provide it to work error free due to how the parsing works so this
API has been deprecated. See :ref:`upgrade-to-3.2` for more information.
- Restored the functionality of `invoked_subcommand` which was broken as
a regression in 3.1.
Version 3.1 Version 3.1
----------- -----------
(bugfix release, released on August 13th 2014) Released 2014-08-13
- Fixed a regression that caused contexts of subcommands to be created
before the parent command was invoked which was a regression from
earlier Click versions.
- Fixed a regression that caused contexts of subcommands to be
created before the parent command was invoked which was a
regression from earlier Click versions.
Version 3.0 Version 3.0
----------- -----------
(codename "clonk clonk", released on August 12th 2014) Released 2014-08-12, codename "clonk clonk"
- Formatter now no longer attempts to accomodate for terminals smaller
than 50 characters. If that happens it just assumes a minimal width.
- Added a way to not swallow exceptions in the test system.
- Added better support for colors with pagers and ways to override the
autodetection.
- The CLI runner's result object now has a traceback attached.
- Improved automatic short help detection to work better with dots
that do not terminate sentences.
- When definining options without actual valid option strings now,
Click will give an error message instead of silently passing. This
should catch situations where users wanted to created arguments
instead of options.
- Restructured Click internally to support vendoring.
- Added support for multi command chaining.
- Added support for defaults on options with ``multiple`` and options
and arguments with ``nargs != 1``.
- Label passed to ``progressbar`` is no longer rendered with
whitespace stripped.
- Added a way to disable the standalone mode of the ``main`` method on
a Click command to be able to handle errors better.
- Added support for returning values from command callbacks.
- Added simplifications for printing to stderr from ``echo``.
- Added result callbacks for groups.
- Entering a context multiple times defers the cleanup until the last
exit occurs.
- Added ``open_file``.
- formatter now no longer attempts to accomodate for terminals
smaller than 50 characters. If that happens it just assumes
a minimal width.
- added a way to not swallow exceptions in the test system.
- added better support for colors with pagers and ways to
override the autodetection.
- the CLI runner's result object now has a traceback attached.
- improved automatic short help detection to work better with
dots that do not terminate sentences.
- when definining options without actual valid option strings
now, Click will give an error message instead of silently
passing. This should catch situations where users wanted to
created arguments instead of options.
- Restructured Click internally to support vendoring.
- Added support for multi command chaining.
- Added support for defaults on options with ``multiple`` and
options and arguments with ``nargs != 1``.
- label passed to ``progressbar`` is no longer rendered with
whitespace stripped.
- added a way to disable the standalone mode of the ``main``
method on a Click command to be able to handle errors better.
- added support for returning values from command callbacks.
- added simplifications for printing to stderr from ``echo``.
- added result callbacks for groups.
- entering a context multiple times defers the cleanup until
the last exit occurs.
- added ``open_file``.
Version 2.6 Version 2.6
----------- -----------
(bugfix release, released on August 11th 2014) Released 2014-08-11
- Fixed an issue where the wrapped streams on Python 3 would be
reporting incorrect values for seekable.
- Fixed an issue where the wrapped streams on Python 3 would be reporting
incorrect values for seekable.
Version 2.5 Version 2.5
----------- -----------
(bugfix release, released on July 28th 2014) Released 2014-07-28
- Fixed a bug with text wrapping on Python 3.
- Fixed a bug with text wrapping on Python 3.
Version 2.4 Version 2.4
----------- -----------
(bugfix release, released on July 4th 2014) Released 2014-07-04
- Corrected a bug in the change of the help option in 2.3.
- Corrected a bug in the change of the help option in 2.3.
Version 2.3 Version 2.3
----------- -----------
(bugfix release, released on July 3rd 2014) Released 2014-07-03
- Fixed an incorrectly formatted help record for count options.
- Add support for ansi code stripping on Windows if colorama is not
available.
- Restored the Click 1.0 handling of the help parameter for certain
edge cases.
- Fixed an incorrectly formatted help record for count options.
- Add support for ansi code stripping on Windows if colorama
is not available.
- restored the Click 1.0 handling of the help parameter for certain
edge cases.
Version 2.2 Version 2.2
----------- -----------
(bugfix release, released on June 26th 2014) Released 2014-06-26
- Fixed tty detection on PyPy.
- Fixed an issue that progress bars were not rendered when the context
manager was entered.
- fixed tty detection on PyPy.
- fixed an issue that progress bars were not rendered when the
context manager was entered.
Version 2.1 Version 2.1
----------- -----------
(bugfix release, released on June 14th 2014) Released 2014-06-14
- Fixed the :func:`launch` function on windows.
- Improved the colorama support on windows to try hard to not screw up
the console if the application is interrupted.
- Fixed windows terminals incorrectly being reported to be 80
characters wide instead of 79
- Use colorama win32 bindings if available to get the correct
dimensions of a windows terminal.
- Fixed an issue with custom function types on Python 3.
- Fixed an issue with unknown options being incorrectly reported in
error messages.
- fixed the :func:`launch` function on windows.
- improved the colorama support on windows to try hard to not
screw up the console if the application is interrupted.
- fixed windows terminals incorrectly being reported to be 80
characters wide instead of 79
- use colorama win32 bindings if available to get the correct
dimensions of a windows terminal.
- fixed an issue with custom function types on Python 3.
- fixed an issue with unknown options being incorrectly reported
in error messages.
Version 2.0 Version 2.0
----------- -----------
(codename "tap tap tap", released on June 6th 2014) Released 2014-06-06, codename "tap tap tap"
- Added support for opening stdin/stdout on Windows in binary mode
correctly.
- Added support for atomic writes to files by going through a
temporary file.
- Introduced :exc:`BadParameter` which can be used to easily perform
custom validation with the same error messages as in the type
system.
- Added :func:`progressbar`; a function to show progress bars.
- Added :func:`get_app_dir`; a function to calculate the home folder
for configs.
- Added transparent handling for ANSI codes into the :func:`echo`
function through ``colorama``.
- Added :func:`clear` function.
- Breaking change: parameter callbacks now get the parameter object
passed as second argument. There is legacy support for old callbacks
which will warn but still execute the script.
- Added :func:`style`, :func:`unstyle` and :func:`secho` for ANSI
styles.
- Added an :func:`edit` function that invokes the default editor.
- Added an :func:`launch` function that launches browsers and
applications.
- Nargs of -1 for arguments can now be forced to be a single item
through the required flag. It defaults to not required.
- Setting a default for arguments now implicitly makes it non
required.
- Changed "yN" / "Yn" to "y/N" and "Y/n" in confirmation prompts.
- Added basic support for bash completion.
- Added :func:`getchar` to fetch a single character from the terminal.
- Errors now go to stderr as intended.
- Fixed various issues with more exotic parameter formats like
DOS/Windows style arguments.
- Added :func:`pause` which works similar to the Windows ``pause`` cmd
built-in but becomes an automatic noop if the application is not run
through a terminal.
- Added a bit of extra information about missing choice parameters.
- Changed how the help function is implemented to allow global
overriding of the help option.
- Added support for token normalization to implement case insensitive
handling.
- Added support for providing defaults for context settings.
- added support for opening stdin/stdout on Windows in
binary mode correctly.
- added support for atomic writes to files by going through
a temporary file.
- introduced :exc:`BadParameter` which can be used to easily perform
custom validation with the same error messages as in the type system.
- added :func:`progressbar`; a function to show progress bars.
- added :func:`get_app_dir`; a function to calculate the home folder
for configs.
- Added transparent handling for ANSI codes into the :func:`echo`
function through ``colorama``.
- Added :func:`clear` function.
- Breaking change: parameter callbacks now get the parameter object
passed as second argument. There is legacy support for old callbacks
which will warn but still execute the script.
- Added :func:`style`, :func:`unstyle` and :func:`secho` for ANSI
styles.
- Added an :func:`edit` function that invokes the default editor.
- Added an :func:`launch` function that launches browsers and applications.
- nargs of -1 for arguments can now be forced to be a single item through
the required flag. It defaults to not required.
- setting a default for arguments now implicitly makes it non required.
- changed "yN" / "Yn" to "y/N" and "Y/n" in confirmation prompts.
- added basic support for bash completion.
- added :func:`getchar` to fetch a single character from the terminal.
- errors now go to stderr as intended.
- fixed various issues with more exotic parameter formats like DOS/Windows
style arguments.
- added :func:`pause` which works similar to the Windows ``pause`` cmd
built-in but becomes an automatic noop if the application is not run
through a terminal.
- added a bit of extra information about missing choice parameters.
- changed how the help function is implemented to allow global overriding
of the help option.
- added support for token normalization to implement case insensitive handling.
- added support for providing defaults for context settings.
Version 1.1 Version 1.1
----------- -----------
(bugfix release, released on May 23rd 2014) Released 2014-05-23
- Fixed a bug that caused text files in Python 2 to not accept native
strings.
- fixed a bug that caused text files in Python 2 to not accept
native strings.
Version 1.0 Version 1.0
----------- -----------
(no codename, released on May 21st 2014) Released 2014-05-21
- Initial release. - Initial release.

View file

@ -1,61 +0,0 @@
==========================
How to contribute to Click
==========================
Thanks for considering contributing to Click.
Support questions
=================
Please, don't use the issue tracker for this. Check whether the
``#pocoo`` IRC channel on Freenode can help with your issue. If your problem
is not strictly Click-specific, ``#python`` on Freenode is generally more
active. `StackOverflow <https://stackoverflow.com/>`_ is also worth
considering.
Reporting issues
================
- Under which versions of Python does this happen? This is even more important
if your issue is encoding related.
- Under which versions of Click does this happen? Check if this issue is fixed
in the repository.
Submitting patches
==================
- Include tests if your patch is supposed to solve a bug, and explain clearly
under which circumstances the bug happens. Make sure the test fails without
your patch.
- Try to follow `PEP8 <http://legacy.python.org/dev/peps/pep-0008/>`_, but you
may ignore the line-length-limit if following it would make the code uglier.
- For features: Consider whether your feature would be a better fit for an
`external package <https://click.palletsprojects.com/en/7.x/contrib/>`_
- For docs and bug fixes: Submit against the latest maintenance branch instead of master!
Running the testsuite
---------------------
You probably want to set up a `virtualenv
<https://virtualenv.readthedocs.io/en/latest/index.html>`_.
The minimal requirement for running the testsuite is ``py.test``. You can
install it with::
pip install pytest
Then you can run the testsuite with::
py.test
For a more isolated test environment, you can also install ``tox`` instead of
``pytest``. You can install it with::
pip install tox
The ``tox`` command will then run all tests against multiple combinations of
Python versions and dependency versions.

View file

@ -1,39 +1,28 @@
Copyright © 2014 by the Pallets team. Copyright 2014 Pallets
Some rights reserved. Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistribution and use in source and binary forms of the software as 1. Redistributions of source code must retain the above copyright
well as documentation, with or without modification, are permitted
provided that the following conditions are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer. notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright 2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution. documentation and/or other materials provided with the distribution.
- Neither the name of the copyright holder nor the names of its 3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from contributors may be used to endorse or promote products derived from
this software without specific prior written permission. this software without specific prior written permission.
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
SUCH DAMAGE.
----
Click uses parts of optparse written by Gregory P. Ward and maintained
by the Python Software Foundation. This is limited to code in parser.py.
Copyright © 2001-2006 Gregory P. Ward. All rights reserved.
Copyright © 2002-2006 Python Software Foundation. All rights reserved.

View file

@ -1,11 +1,8 @@
include CHANGES.rst include CHANGES.rst
include CONTRIBUTING.rst
include LICENSE.rst
include README.rst
include tox.ini include tox.ini
graft artwork graft artwork
graft docs graft docs
prune docs/_build prune docs/_build
graft examples graft examples
graft tests graft tests
global-exclude *.py[co] .DS_Store global-exclude *.pyc

View file

@ -1,13 +1,11 @@
Metadata-Version: 1.2 Metadata-Version: 1.2
Name: Click Name: click
Version: 7.0 Version: 7.1.2
Summary: Composable command line interface toolkit Summary: Composable command line interface toolkit
Home-page: https://palletsprojects.com/p/click/ Home-page: https://palletsprojects.com/p/click/
Author: Armin Ronacher Maintainer: Pallets
Author-email: armin.ronacher@active-4.com
Maintainer: Pallets Team
Maintainer-email: contact@palletsprojects.com Maintainer-email: contact@palletsprojects.com
License: BSD License: BSD-3-Clause
Project-URL: Documentation, https://click.palletsprojects.com/ Project-URL: Documentation, https://click.palletsprojects.com/
Project-URL: Code, https://github.com/pallets/click Project-URL: Code, https://github.com/pallets/click
Project-URL: Issue tracker, https://github.com/pallets/click/issues Project-URL: Issue tracker, https://github.com/pallets/click/issues
@ -37,9 +35,7 @@ Description: \$ click\_
.. code-block:: text .. code-block:: text
$ pip install click $ pip install -U click
Click supports Python 3.4 and newer, Python 2.7, and PyPy.
.. _pip: https://pip.pypa.io/en/stable/quickstart/ .. _pip: https://pip.pypa.io/en/stable/quickstart/
@ -47,26 +43,21 @@ Description: \$ click\_
A Simple Example A Simple Example
---------------- ----------------
What does it look like? Here is an example of a simple Click program:
.. code-block:: python .. code-block:: python
import click import click
@click.command() @click.command()
@click.option("--count", default=1, help="Number of greetings.") @click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name", @click.option("--name", prompt="Your name", help="The person to greet.")
help="The person to greet.")
def hello(count, name): def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times.""" """Simple program that greets NAME for a total of COUNT times."""
for _ in range(count): for _ in range(count):
click.echo("Hello, %s!" % name) click.echo(f"Hello, {name}!")
if __name__ == '__main__': if __name__ == '__main__':
hello() hello()
And what it looks like when run:
.. code-block:: text .. code-block:: text
$ python hello.py --count=3 $ python hello.py --count=3
@ -90,18 +81,13 @@ Description: \$ click\_
Links Links
----- -----
* Website: https://palletsprojects.com/p/click/ - Website: https://palletsprojects.com/p/click/
* Documentation: https://click.palletsprojects.com/ - Documentation: https://click.palletsprojects.com/
* License: `BSD <https://github.com/pallets/click/blob/master/LICENSE.rst>`_ - Releases: https://pypi.org/project/click/
* Releases: https://pypi.org/project/click/ - Code: https://github.com/pallets/click
* Code: https://github.com/pallets/click - Issue tracker: https://github.com/pallets/click/issues
* Issue tracker: https://github.com/pallets/click/issues - Test status: https://dev.azure.com/pallets/click/_build
* Test status: - Official chat: https://discord.gg/t6rrQZH
* Linux, Mac: https://travis-ci.org/pallets/click
* Windows: https://ci.appveyor.com/project/pallets/click
* Test coverage: https://codecov.io/gh/pallets/click
Platform: UNKNOWN Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 5 - Production/Stable
@ -110,10 +96,5 @@ Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*

View file

@ -24,9 +24,7 @@ Install and update using `pip`_:
.. code-block:: text .. code-block:: text
$ pip install click $ pip install -U click
Click supports Python 3.4 and newer, Python 2.7, and PyPy.
.. _pip: https://pip.pypa.io/en/stable/quickstart/ .. _pip: https://pip.pypa.io/en/stable/quickstart/
@ -34,26 +32,21 @@ Click supports Python 3.4 and newer, Python 2.7, and PyPy.
A Simple Example A Simple Example
---------------- ----------------
What does it look like? Here is an example of a simple Click program:
.. code-block:: python .. code-block:: python
import click import click
@click.command() @click.command()
@click.option("--count", default=1, help="Number of greetings.") @click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name", @click.option("--name", prompt="Your name", help="The person to greet.")
help="The person to greet.")
def hello(count, name): def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times.""" """Simple program that greets NAME for a total of COUNT times."""
for _ in range(count): for _ in range(count):
click.echo("Hello, %s!" % name) click.echo(f"Hello, {name}!")
if __name__ == '__main__': if __name__ == '__main__':
hello() hello()
And what it looks like when run:
.. code-block:: text .. code-block:: text
$ python hello.py --count=3 $ python hello.py --count=3
@ -77,15 +70,10 @@ donate today`_.
Links Links
----- -----
* Website: https://palletsprojects.com/p/click/ - Website: https://palletsprojects.com/p/click/
* Documentation: https://click.palletsprojects.com/ - Documentation: https://click.palletsprojects.com/
* License: `BSD <https://github.com/pallets/click/blob/master/LICENSE.rst>`_ - Releases: https://pypi.org/project/click/
* Releases: https://pypi.org/project/click/ - Code: https://github.com/pallets/click
* Code: https://github.com/pallets/click - Issue tracker: https://github.com/pallets/click/issues
* Issue tracker: https://github.com/pallets/click/issues - Test status: https://dev.azure.com/pallets/click/_build
* Test status: - Official chat: https://discord.gg/t6rrQZH
* Linux, Mac: https://travis-ci.org/pallets/click
* Windows: https://ci.appveyor.com/project/pallets/click
* Test coverage: https://codecov.io/gh/pallets/click

View file

@ -1,97 +0,0 @@
# -*- coding: utf-8 -*-
"""
click
~~~~~
Click is a simple Python module inspired by the stdlib optparse to make
writing command line scripts fun. Unlike other modules, it's based
around a simple API that does not come with too much magic and is
composable.
:copyright: © 2014 by the Pallets team.
:license: BSD, see LICENSE.rst for more details.
"""
# Core classes
from .core import Context, BaseCommand, Command, MultiCommand, Group, \
CommandCollection, Parameter, Option, Argument
# Globals
from .globals import get_current_context
# Decorators
from .decorators import pass_context, pass_obj, make_pass_decorator, \
command, group, argument, option, confirmation_option, \
password_option, version_option, help_option
# Types
from .types import ParamType, File, Path, Choice, IntRange, Tuple, \
DateTime, STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED, FloatRange
# Utilities
from .utils import echo, get_binary_stream, get_text_stream, open_file, \
format_filename, get_app_dir, get_os_args
# Terminal functions
from .termui import prompt, confirm, get_terminal_size, echo_via_pager, \
progressbar, clear, style, unstyle, secho, edit, launch, getchar, \
pause
# Exceptions
from .exceptions import ClickException, UsageError, BadParameter, \
FileError, Abort, NoSuchOption, BadOptionUsage, BadArgumentUsage, \
MissingParameter
# Formatting
from .formatting import HelpFormatter, wrap_text
# Parsing
from .parser import OptionParser
__all__ = [
# Core classes
'Context', 'BaseCommand', 'Command', 'MultiCommand', 'Group',
'CommandCollection', 'Parameter', 'Option', 'Argument',
# Globals
'get_current_context',
# Decorators
'pass_context', 'pass_obj', 'make_pass_decorator', 'command', 'group',
'argument', 'option', 'confirmation_option', 'password_option',
'version_option', 'help_option',
# Types
'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'Tuple',
'DateTime', 'STRING', 'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED',
'FloatRange',
# Utilities
'echo', 'get_binary_stream', 'get_text_stream', 'open_file',
'format_filename', 'get_app_dir', 'get_os_args',
# Terminal functions
'prompt', 'confirm', 'get_terminal_size', 'echo_via_pager',
'progressbar', 'clear', 'style', 'unstyle', 'secho', 'edit', 'launch',
'getchar', 'pause',
# Exceptions
'ClickException', 'UsageError', 'BadParameter', 'FileError',
'Abort', 'NoSuchOption', 'BadOptionUsage', 'BadArgumentUsage',
'MissingParameter',
# Formatting
'HelpFormatter', 'wrap_text',
# Parsing
'OptionParser',
]
# Controls if click should emit the warning about the use of unicode
# literals.
disable_unicode_literals_warning = False
__version__ = '7.0'

View file

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

View file

@ -12,9 +12,11 @@ Click. This page should give some insight into what can be accomplished.
Command Aliases Command Aliases
--------------- ---------------
Many tools support aliases for commands. For instance, you can configure Many tools support aliases for commands (see `Command alias example
``git`` to accept ``git ci`` as alias for ``git commit``. Other tools <https://github.com/pallets/click/tree/master/examples/aliases>`_).
also support auto-discovery for aliases by automatically shortening them. For instance, you can configure ``git`` to accept ``git ci`` as alias for
``git commit``. Other tools also support auto-discovery for aliases by
automatically shortening them.
Click does not support this out of the box, but it's very easy to customize Click does not support this out of the box, but it's very easy to customize
the :class:`Group` or any other :class:`MultiCommand` to provide this the :class:`Group` or any other :class:`MultiCommand` to provide this

View file

@ -8,8 +8,8 @@ Arguments
Arguments work similarly to :ref:`options <options>` but are positional. Arguments work similarly to :ref:`options <options>` but are positional.
They also only support a subset of the features of options due to their 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 syntactical nature. Click will also not attempt to document arguments for
you and wants you to document them manually in order to avoid ugly help you and wants you to :ref:`document them manually <documenting-arguments>`
pages. in order to avoid ugly help pages.
Basic Arguments Basic Arguments
--------------- ---------------
@ -25,6 +25,7 @@ Example:
@click.command() @click.command()
@click.argument('filename') @click.argument('filename')
def touch(filename): def touch(filename):
"""Print FILENAME."""
click.echo(filename) click.echo(filename)
And what it looks like: And what it looks like:
@ -52,6 +53,7 @@ Example:
@click.argument('src', nargs=-1) @click.argument('src', nargs=-1)
@click.argument('dst', nargs=1) @click.argument('dst', nargs=1)
def copy(src, dst): def copy(src, dst):
"""Move file SRC to DST."""
for fn in src: for fn in src:
click.echo('move %s to folder %s' % (fn, dst)) click.echo('move %s to folder %s' % (fn, dst))
@ -101,6 +103,7 @@ Example:
@click.argument('input', type=click.File('rb')) @click.argument('input', type=click.File('rb'))
@click.argument('output', type=click.File('wb')) @click.argument('output', type=click.File('wb'))
def inout(input, output): def inout(input, output):
"""Copy contents of INPUT to OUTPUT."""
while True: while True:
chunk = input.read(1024) chunk = input.read(1024)
if not chunk: if not chunk:
@ -136,9 +139,10 @@ Example:
.. click:example:: .. click:example::
@click.command() @click.command()
@click.argument('f', type=click.Path(exists=True)) @click.argument('filename', type=click.Path(exists=True))
def touch(f): def touch(filename):
click.echo(click.format_filename(f)) """Print FILENAME if the file exists."""
click.echo(click.format_filename(filename))
And what it does: And what it does:
@ -199,6 +203,7 @@ Example usage:
@click.command() @click.command()
@click.argument('src', envvar='SRC', type=click.File('r')) @click.argument('src', envvar='SRC', type=click.File('r'))
def echo(src): def echo(src):
"""Print value of SRC environment variable."""
click.echo(src.read()) click.echo(src.read())
And from the command line: And from the command line:
@ -235,6 +240,7 @@ Example usage:
@click.command() @click.command()
@click.argument('files', nargs=-1, type=click.Path()) @click.argument('files', nargs=-1, type=click.Path())
def touch(files): def touch(files):
"""Print all FILES file names."""
for filename in files: for filename in files:
click.echo(filename) click.echo(filename)
@ -252,6 +258,7 @@ True to avoid checking unknown options:
@click.command(context_settings={"ignore_unknown_options": True}) @click.command(context_settings={"ignore_unknown_options": True})
@click.argument('files', nargs=-1, type=click.Path()) @click.argument('files', nargs=-1, type=click.Path())
def touch(files): def touch(files):
"""Print all FILES file names."""
for filename in files: for filename in files:
click.echo(filename) click.echo(filename)
@ -260,4 +267,3 @@ And from the command line:
.. click:run:: .. click:run::
invoke(touch, ['-foo.txt', 'bar.txt']) invoke(touch, ['-foo.txt', 'bar.txt'])

View file

@ -1,47 +1,46 @@
Bash Complete Shell Completion
============= ================
.. versionadded:: 2.0 .. versionadded:: 2.0
As of Click 2.0, there is built-in support for Bash completion for Click can provide tab completion for commands, options, and choice
any Click script. There are certain restrictions on when this completion values. Bash, Zsh, and Fish are supported
is available, but for the most part it should just work.
Limitations Completion is only available if a script is installed and invoked
----------- through an entry point, not through the ``python`` command. See
:ref:`setuptools-integration`.
Bash completion is only available if a script has been installed properly,
and not executed through the ``python`` command. For information about
how to do that, see :ref:`setuptools-integration`. Click currently
only supports completion for Bash and Zsh.
What it Completes What it Completes
----------------- -----------------
Generally, the Bash completion support will complete subcommands, options Generally, the shell completion support will complete commands,
and any option or argument values where the type is click.Choice. options, and any option or argument values where the type is
Subcommands and choices are always listed whereas options only if at :class:`click.Choice`. Options are only listed if at least a dash has
least a dash has been provided. Example:: been entered.
.. code-block:: text
$ repo <TAB><TAB> $ repo <TAB><TAB>
clone commit copy delete setuser clone commit copy delete setuser
$ repo clone -<TAB><TAB> $ repo clone -<TAB><TAB>
--deep --help --rev --shallow -r --deep --help --rev --shallow -r
Additionally, custom suggestions can be provided for arguments and options with Custom completions can be provided for argument and option values by
the ``autocompletion`` parameter. ``autocompletion`` should a callback function providing an ``autocompletion`` function that returns a list of strings.
that returns a list of strings. This is useful when the suggestions need to be This is useful when the suggestions need to be dynamically generated
dynamically generated at bash completion time. The callback function will be completion time. The callback function will be passed 3 keyword
passed 3 keyword arguments: arguments:
- ``ctx`` - The current click context. - ``ctx`` - The current command context.
- ``args`` - The list of arguments passed in. - ``args`` - The list of arguments passed in.
- ``incomplete`` - The partial word that is being completed, as a string. May - ``incomplete`` - The partial word that is being completed. May
be an empty string ``''`` if no characters have been entered yet. be an empty string if no characters have been entered yet.
Here is an example of using a callback function to generate dynamic suggestions: Here is an example of using a callback function to generate dynamic
suggestions:
.. click:example:: .. code-block:: python
import os import os
@ -55,25 +54,26 @@ Here is an example of using a callback function to generate dynamic suggestions:
click.echo('Value: %s' % os.environ[envvar]) click.echo('Value: %s' % os.environ[envvar])
Completion help strings (ZSH only) Completion help strings
---------------------------------- -----------------------
ZSH supports showing documentation strings for completions. These are taken ZSH and fish support showing documentation strings for completions.
from the help parameters of options and subcommands. For dynamically generated These are taken from the help parameters of options and subcommands. For
completions a help string can be provided by returning a tuple instead of a dynamically generated completions a help string can be provided by
string. The first element of the tuple is the completion and the second is the returning a tuple instead of a string. The first element of the tuple is
help string to display. the completion and the second is the help string to display.
Here is an example of using a callback function to generate dynamic suggestions with help strings: Here is an example of using a callback function to generate dynamic
suggestions with help strings:
.. click:example:: .. code-block:: python
import os import os
def get_colors(ctx, args, incomplete): def get_colors(ctx, args, incomplete):
colors = [('red', 'help string for the color red'), colors = [('red', 'a warm color'),
('blue', 'help string for the color blue'), ('blue', 'a cool color'),
('green', 'help string for the color green')] ('green', 'the other starter color')]
return [c for c in colors if incomplete in c[0]] return [c for c in colors if incomplete in c[0]]
@click.command() @click.command()
@ -85,47 +85,79 @@ Here is an example of using a callback function to generate dynamic suggestions
Activation Activation
---------- ----------
In order to activate Bash completion, you need to inform Bash that In order to activate shell completion, you need to inform your shell
completion is available for your script, and how. Any Click application that completion is available for your script. Any Click application
automatically provides support for that. The general way this works is automatically provides support for that. If the program is executed with
through a magic environment variable called ``_<PROG_NAME>_COMPLETE``, a special ``_<PROG_NAME>_COMPLETE`` variable, the completion mechanism
where ``<PROG_NAME>`` is your application executable name in uppercase is triggered instead of the normal command. ``<PROG_NAME>`` is the
with dashes replaced by underscores. executable name in uppercase with dashes replaced by underscores.
If your tool is called ``foo-bar``, then the magic variable is called If your tool is called ``foo-bar``, then the variable is called
``_FOO_BAR_COMPLETE``. By exporting it with the ``source`` value it will ``_FOO_BAR_COMPLETE``. By exporting it with the ``source_{shell}``
spit out the activation script which can be trivially activated. value it will output the activation script to evaluate.
For instance, to enable Bash completion for your ``foo-bar`` script, this Here are examples for a ``foo-bar`` script.
is what you would need to put into your ``.bashrc``::
eval "$(_FOO_BAR_COMPLETE=source foo-bar)" For Bash, add this to ``~/.bashrc``:
For zsh users add this to your ``.zshrc``:: .. code-block:: text
eval "$(_FOO_BAR_COMPLETE=source_bash foo-bar)"
For Zsh, add this to ``~/.zshrc``:
.. code-block:: text
eval "$(_FOO_BAR_COMPLETE=source_zsh foo-bar)" eval "$(_FOO_BAR_COMPLETE=source_zsh foo-bar)"
From this point onwards, your script will have autocompletion enabled. For Fish, add this to ``~/.config/fish/completions/foo-bar.fish``:
.. code-block:: text
eval (env _FOO_BAR_COMPLETE=source_fish foo-bar)
Open a new shell to enable completion. Or run the ``eval`` command
directly in your current shell to enable it temporarily.
Activation Script Activation Script
----------------- -----------------
The above activation example will always invoke your application on The above ``eval`` examples will invoke your application every time a
startup. This might be slowing down the shell activation time shell is started. This may slow down shell startup time significantly.
significantly if you have many applications. Alternatively, you could also
ship a file with the contents of that, which is what Git and other systems
are doing.
This can be easily accomplished:: Alternatively, export the generated completion code as a static script
to be executed. You can ship this file with your builds; tools like Git
do this. At least Zsh will also cache the results of completion files,
but not ``eval`` scripts.
_FOO_BAR_COMPLETE=source foo-bar > foo-bar-complete.sh For Bash:
For zsh: .. code-block:: text
_FOO_BAR_COMPLETE=source_bash foo-bar > foo-bar-complete.sh
For Zsh:
.. code-block:: text
_FOO_BAR_COMPLETE=source_zsh foo-bar > foo-bar-complete.sh _FOO_BAR_COMPLETE=source_zsh foo-bar > foo-bar-complete.sh
And then you would put this into your .bashrc or .zshrc instead:: For Fish:
.. code-block:: text
_FOO_BAR_COMPLETE=source_zsh foo-bar > foo-bar-complete.sh
In ``.bashrc`` or ``.zshrc``, source the script instead of the ``eval``
command:
.. code-block:: text
. /path/to/foo-bar-complete.sh . /path/to/foo-bar-complete.sh
For Fish, add the file to the completions directory:
.. code-block:: text
_FOO_BAR_COMPLETE=source_fish foo-bar > ~/.config/fish/completions/foo-bar-complete.fish

View file

@ -1,3 +1,4 @@
.. currentmodule:: click Changes
=======
.. include:: ../CHANGES.rst .. include:: ../CHANGES.rst

View file

@ -27,7 +27,7 @@ when an inner command runs:
def cli(debug): def cli(debug):
click.echo('Debug mode is %s' % ('on' if debug else 'off')) click.echo('Debug mode is %s' % ('on' if debug else 'off'))
@cli.command() @cli.command() # @cli, not @click!
def sync(): def sync():
click.echo('Syncing') click.echo('Syncing')
@ -88,7 +88,7 @@ script like this:
@click.pass_context @click.pass_context
def cli(ctx, debug): def cli(ctx, debug):
# ensure that ctx.obj exists and is a dict (in case `cli()` is called # ensure that ctx.obj exists and is a dict (in case `cli()` is called
# by means other than the `if` block below # by means other than the `if` block below)
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.obj['DEBUG'] = debug ctx.obj['DEBUG'] = debug
@ -317,7 +317,8 @@ When using multi command chaining you can only have one command (the last)
use ``nargs=-1`` on an argument. It is also not possible to nest multi use ``nargs=-1`` on an argument. It is also not possible to nest multi
commands below chained multicommands. Other than that there are no commands below chained multicommands. Other than that there are no
restrictions on how they work. They can accept options and arguments as restrictions on how they work. They can accept options and arguments as
normal. normal. The order between options and arguments is limited for chained
commands. Currently only ``--options argument`` order is allowed.
Another note: the :attr:`Context.invoked_subcommand` attribute is a bit Another note: the :attr:`Context.invoked_subcommand` attribute is a bit
useless for multi commands as it will give ``'*'`` as value if more than useless for multi commands as it will give ``'*'`` as value if more than
@ -439,10 +440,18 @@ defaults.
This is useful if you plug in some commands from another package but This is useful if you plug in some commands from another package but
you're not satisfied with the defaults. you're not satisfied with the defaults.
The default map can be nested arbitrarily for each subcommand and The default map can be nested arbitrarily for each subcommand:
provided when the script is invoked. Alternatively, it can also be
overridden at any point by commands. For instance, a top-level command could .. code-block:: python
load the defaults from a configuration file.
default_map = {
"debug": True, # default for a top level option
"runserver": {"port": 5000} # default for a subcommand
}
The default map can be provided when the script is invoked, or
overridden at any point by commands. For instance, a top-level command
could load the defaults from a configuration file.
Example usage: Example usage:

View file

@ -1,17 +1,25 @@
from pallets_sphinx_themes import ProjectLink, get_version from pallets_sphinx_themes import get_version
from pallets_sphinx_themes import ProjectLink
# Project -------------------------------------------------------------- # Project --------------------------------------------------------------
project = "Click" project = "Click"
copyright = "2014 Pallets Team" copyright = "2014 Pallets"
author = "Pallets Team" author = "Pallets"
release, version = get_version("Click", version_length=1) release, version = get_version("Click", version_length=1)
# General -------------------------------------------------------------- # General --------------------------------------------------------------
master_doc = "index" master_doc = "index"
extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "pallets_sphinx_themes"] extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinxcontrib.log_cabinet",
"pallets_sphinx_themes",
"sphinx_issues",
]
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
issues_github_path = "pallets/click"
# HTML ----------------------------------------------------------------- # HTML -----------------------------------------------------------------
@ -21,26 +29,22 @@ html_context = {
"project_links": [ "project_links": [
ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"), ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"),
ProjectLink("Click Website", "https://palletsprojects.com/p/click/"), ProjectLink("Click Website", "https://palletsprojects.com/p/click/"),
ProjectLink("PyPI releases", "https://pypi.org/project/Click/"), ProjectLink("PyPI releases", "https://pypi.org/project/click/"),
ProjectLink("Source Code", "https://github.com/pallets/click/"), ProjectLink("Source Code", "https://github.com/pallets/click/"),
ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"), ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"),
] ]
} }
html_sidebars = { html_sidebars = {
"index": ["project.html", "versions.html", "searchbox.html"], "index": ["project.html", "localtoc.html", "searchbox.html"],
"**": ["localtoc.html", "relations.html", "versions.html", "searchbox.html"], "**": ["localtoc.html", "relations.html", "searchbox.html"],
} }
singlehtml_sidebars = {"index": ["project.html", "versions.html", "localtoc.html"]} singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]}
html_static_path = ["_static"] html_static_path = ["_static"]
html_favicon = "_static/click-icon.png" html_favicon = "_static/click-icon.png"
html_logo = "_static/click-logo-sidebar.png" html_logo = "_static/click-logo-sidebar.png"
html_title = "Click Documentation ({})".format(version) html_title = f"Click Documentation ({version})"
html_show_sourcelink = False html_show_sourcelink = False
html_domain_indices = False
html_experimental_html5_writer = True
# LaTeX ---------------------------------------------------------------- # LaTeX ----------------------------------------------------------------
latex_documents = [ latex_documents = [(master_doc, f"Click-{version}.tex", html_title, author, "manual")]
(master_doc, "Click-{}.tex".format(version), html_title, author, "manual")
]

View file

@ -32,10 +32,54 @@ And what it looks like:
invoke(hello, args=['--help']) invoke(hello, args=['--help'])
Arguments cannot be documented this way. This is to follow the general
convention of Unix tools of using arguments for only the most necessary .. _documenting-arguments:
things and to document them in the introduction text by referring to them
by name. Documenting Arguments
~~~~~~~~~~~~~~~~~~~~~
:func:`click.argument` does not take a ``help`` parameter. This is to
follow the general convention of Unix tools of using arguments for only
the most necessary things, and to document them in the command help text
by referring to them by name.
You might prefer to reference the argument in the description:
.. click:example::
@click.command()
@click.argument('filename')
def touch(filename):
"""Print FILENAME."""
click.echo(filename)
And what it looks like:
.. click:run::
invoke(touch, args=['--help'])
Or you might prefer to explicitly provide a description of the argument:
.. click:example::
@click.command()
@click.argument('filename')
def touch(filename):
"""Print FILENAME.
FILENAME is the name of the file to check.
"""
click.echo(filename)
And what it looks like:
.. click:run::
invoke(touch, args=['--help'])
For more examples, see the examples in :doc:`/arguments`.
Preventing Rewrapping Preventing Rewrapping
--------------------- ---------------------

View file

@ -1,14 +1,4 @@
License BSD-3-Clause License
======= ====================
Click is licensed under a three-clause BSD License. It basically means:
do whatever you want with it as long as the copyright in Click sticks
around, the conditions are not modified and the disclaimer is present.
Furthermore, you must not use the names of the authors to promote derivatives
of the software without written consent.
License Text
------------
.. include:: ../LICENSE.rst .. include:: ../LICENSE.rst

View file

@ -13,25 +13,45 @@ distinct from :ref:`positional arguments <arguments>`.
Name Your Options Name Your Options
----------------- -----------------
The naming rules can be found in :ref:`parameter_names`. In short, you Options have a name that will be used as the Python argument name when
can refer the option **implicitly** by the longest dash-prefixed argument: calling the decorated function. This can be inferred from the option
names or given explicitly. Names are given as position arguments to the
decorator.
.. click:example:: A name is chosen in the following order
1. If a name is not prefixed, it is used as the Python argument name
and not treated as an option name on the command line.
2. If there is at least one name prefixed with two dashes, the first
one given is used as the name.
3. The first name prefixed with one dash is used otherwise.
To get the Python argument name, the chosen name is converted to lower
case, up to two dashes are removed as the prefix, and other dashes are
converted to underscores.
.. code-block:: python
@click.command() @click.command()
@click.option('-s', '--string-to-echo') @click.option('-s', '--string-to-echo')
def echo(string_to_echo): def echo(string_to_echo):
click.echo(string_to_echo) click.echo(string_to_echo)
Or, **explicitly**, by giving one non-dash-prefixed argument: .. code-block:: python
.. click:example::
@click.command() @click.command()
@click.option('-s', '--string-to-echo', 'string') @click.option('-s', '--string-to-echo', 'string')
def echo(string): def echo(string):
click.echo(string) click.echo(string)
- ``"-f", "--foo-bar"``, the name is ``foo_bar``
- ``"-x"``, the name is ``x``
- ``"-f", "--filename", "dest"``, the name is ``dest``
- ``"--CamelCase"``, the name is ``camelcase``
- ``"-f", "-fb"``, the name is ``f``
- ``"--f", "--foo-bar"``, the name is ``f``
- ``"---f"``, the name is ``_f``
Basic Value Options Basic Value Options
------------------- -------------------
@ -145,11 +165,13 @@ used. The above example is thus equivalent to this:
def putitem(item): def putitem(item):
click.echo('name=%s id=%d' % item) click.echo('name=%s id=%d' % item)
.. _multiple-options:
Multiple Options Multiple Options
---------------- ----------------
Similarly to ``nargs``, there is also the case of wanting to support a Similarly to ``nargs``, there is also the case of wanting to support a
parameter being provided multiple times to and have all values recorded -- parameter being provided multiple times and have all the values recorded --
not just the last one. For instance, ``git commit -m foo -m bar`` would not just the last one. For instance, ``git commit -m foo -m bar`` would
record two lines for the commit message: ``foo`` and ``bar``. This can be record two lines for the commit message: ``foo`` and ``bar``. This can be
accomplished with the ``multiple`` flag: accomplished with the ``multiple`` flag:
@ -169,6 +191,15 @@ And on the command line:
invoke(commit, args=['-m', 'foo', '-m', 'bar']) invoke(commit, args=['-m', 'foo', '-m', 'bar'])
When passing a ``default`` with ``multiple=True``, the default value
must be a list or tuple, otherwise it will be interpreted as a list of
single characters.
.. code-block:: python
@click.option("--format", multiple=True, default=["json"])
Counting Counting
-------- --------
@ -315,14 +346,17 @@ Choice Options
Sometimes, you want to have a parameter be a choice of a list of values. Sometimes, you want to have a parameter be a choice of a list of values.
In that case you can use :class:`Choice` type. It can be instantiated In that case you can use :class:`Choice` type. It can be instantiated
with a list of valid values. with a list of valid values. The originally passed choice will be returned,
not the str passed on the command line. Token normalization functions and
``case_sensitive=False`` can cause the two to be different but still match.
Example: Example:
.. click:example:: .. click:example::
@click.command() @click.command()
@click.option('--hash-type', type=click.Choice(['md5', 'sha1'])) @click.option('--hash-type',
type=click.Choice(['MD5', 'SHA1'], case_sensitive=False))
def digest(hash_type): def digest(hash_type):
click.echo(hash_type) click.echo(hash_type)
@ -330,16 +364,27 @@ What it looks like:
.. click:run:: .. click:run::
invoke(digest, args=['--hash-type=MD5'])
println()
invoke(digest, args=['--hash-type=md5']) invoke(digest, args=['--hash-type=md5'])
println() println()
invoke(digest, args=['--hash-type=foo']) invoke(digest, args=['--hash-type=foo'])
println() println()
invoke(digest, args=['--help']) invoke(digest, args=['--help'])
.. note:: Only pass the choices as list or tuple. Other iterables (like
generators) may lead to unexpected results.
You should only pass the choices as list or tuple. Other iterables (like Choices work with options that have ``multiple=True``. If a ``default``
generators) may lead to surprising results. value is given with ``multiple=True``, it should be a list or tuple of
valid choices.
Choices should be unique after considering the effects of
``case_sensitive`` and any specified token normalization function.
.. versionchanged:: 7.1
The resulting value from an option will always be one of the
originally passed choices regardless of ``case_sensitive``.
.. _option-prompting: .. _option-prompting:
@ -382,6 +427,10 @@ What it looks like:
invoke(hello, input=['John']) invoke(hello, input=['John'])
It is advised that prompt not be used in conjunction with the multiple
flag set to True. Instead, prompt in the function interactively.
Password Prompts Password Prompts
---------------- ----------------
@ -566,8 +615,8 @@ environment variables which is supported for options only. To enable this
feature, the ``auto_envvar_prefix`` parameter needs to be passed to the feature, the ``auto_envvar_prefix`` parameter needs to be passed to the
script that is invoked. Each command and parameter is then added as an script that is invoked. Each command and parameter is then added as an
uppercase underscore-separated variable. If you have a subcommand uppercase underscore-separated variable. If you have a subcommand
called ``foo`` taking an option called ``bar`` and the prefix is called ``run`` taking an option called ``reload`` and the prefix is
``MY_TOOL``, then the variable is ``MY_TOOL_FOO_BAR``. ``WEB``, then the variable is ``WEB_RUN_RELOAD``.
Example usage: Example usage:
@ -588,8 +637,11 @@ And from the command line:
invoke(greet, env={'GREETER_USERNAME': 'john'}, invoke(greet, env={'GREETER_USERNAME': 'john'},
auto_envvar_prefix='GREETER') auto_envvar_prefix='GREETER')
When using ``auto_envvar_prefix`` with command groups, the command name needs When using ``auto_envvar_prefix`` with command groups, the command name
to be included in the environment variable, between the prefix and the parameter name, *i.e.* *PREFIX_COMMAND_VARIABLE*. needs to be included in the environment variable, between the prefix and
the parameter name, *i.e.* ``PREFIX_COMMAND_VARIABLE``. If you have a
subcommand called ``run-server`` taking an option called ``host`` and
the prefix is ``WEB``, then the variable is ``WEB_RUN_SERVER_HOST``.
Example: Example:

View file

@ -23,12 +23,13 @@ available for options:
* act as flags (boolean or otherwise) * act as flags (boolean or otherwise)
* option values can be pulled from environment variables, arguments can not * option values can be pulled from environment variables, arguments can not
* options are fully documented in the help page, arguments are not * options are fully documented in the help page, arguments are not
(this is intentional as arguments might be too specific to be (:ref:`this is intentional <documenting-arguments>` as arguments
automatically documented) might be too specific to be automatically documented)
On the other hand arguments, unlike options, can accept an arbitrary number On the other hand arguments, unlike options, can accept an arbitrary number
of arguments. Options can strictly ever only accept a fixed number of of arguments. Options can strictly ever only accept a fixed number of
arguments (defaults to 1). arguments (defaults to 1), or they may be specified multiple times using
:ref:`multiple-options`.
Parameter Types Parameter Types
--------------- ---------------
@ -82,58 +83,60 @@ fails with a `ValueError` is also supported, though discouraged.
Parameter Names Parameter Names
--------------- ---------------
Parameters (both options and arguments) accept a number of positional arguments Parameters (both options and arguments) have a name that will be used as
which are passed to the command function as parameters. Each string with a the Python argument name when calling the decorated function with
single dash is added as a short argument; each string starting with a double values.
dash as a long one.
If a string is added without any dashes, it becomes the internal parameter name Arguments take only one positional name. To provide a different name for
which is also used as variable name. use in help text, see :ref:`doc-meta-variables`.
If all names for a parameter contain dashes, the internal name is generated Options can have many names that may be prefixed with one or two dashes.
automatically by taking the longest argument and converting all dashes to Names with one dash are parsed as short options, names with two are
underscores. parsed as long options. If a name is not prefixed, it is used as the
Python argument name and not parsed as an option name. Otherwise, the
first name with a two dash prefix is used, or the first with a one dash
prefix if there are none with two. The prefix is removed and dashes are
converted to underscores to get the Python argument name.
The internal name is converted to lowercase.
Examples:
* For an option with ``('-f', '--foo-bar')``, the parameter name is `foo_bar`.
* For an option with ``('-x',)``, the parameter is `x`.
* For an option with ``('-f', '--filename', 'dest')``, the parameter name is `dest`.
* For an option with ``('--CamelCaseOption',)``, the parameter is `camelcaseoption`.
* For an arguments with ``(`foogle`)``, the parameter name is `foogle`. To
provide a different human readable name for use in help text, see the section
about :ref:`doc-meta-variables`.
Implementing Custom Types Implementing Custom Types
------------------------- -------------------------
To implement a custom type, you need to subclass the :class:`ParamType` To implement a custom type, you need to subclass the :class:`ParamType`
class. Types can be invoked with or without context and parameter object, class. Override the :meth:`~ParamType.convert` method to convert the
which is why they need to be able to deal with this. value from a string to the correct type.
The following code implements an integer type that accepts hex and octal The following code implements an integer type that accepts hex and octal
numbers in addition to normal integers, and converts them into regular numbers in addition to normal integers, and converts them into regular
integers:: integers.
.. code-block:: python
import click import click
class BasedIntParamType(click.ParamType): class BasedIntParamType(click.ParamType):
name = 'integer' name = "integer"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
try: try:
if value[:2].lower() == '0x': if value[:2].lower() == "0x":
return int(value[2:], 16) return int(value[2:], 16)
elif value[:1] == '0': elif value[:1] == "0":
return int(value, 8) return int(value, 8)
return int(value, 10) return int(value, 10)
except TypeError:
self.fail(
"expected string for int() conversion, got "
f"{value!r} of type {type(value).__name__}",
param,
ctx,
)
except ValueError: except ValueError:
self.fail('%s is not a valid integer' % value, param, ctx) self.fail(f"{value!r} is not a valid integer", param, ctx)
BASED_INT = BasedIntParamType() BASED_INT = BasedIntParamType()
As you can see, a subclass needs to implement the :meth:`ParamType.convert` The :attr:`~ParamType.name` attribute is optional and is used for
method and optionally provide the :attr:`ParamType.name` attribute. The documentation. Call :meth:`~ParamType.fail` if conversion fails. The
latter can be used for documentation purposes. ``param`` and ``ctx`` arguments may be ``None`` in some cases such as
prompts.

View file

@ -8,9 +8,6 @@ it suffers from the Unicode text model in Python 3. All examples in the
documentation were written so that they could run on both Python 2.x and documentation were written so that they could run on both Python 2.x and
Python 3.4 or higher. Python 3.4 or higher.
At the moment, it is strongly recommended to use Python 2 for Click
utilities unless Python 3 is a hard requirement.
.. _python3-limitations: .. _python3-limitations:
Python 3 Limitations Python 3 Limitations
@ -123,6 +120,12 @@ If you see something like this error in Python 3::
to Python 2 or consult the Python 3 section of the docs for to Python 2 or consult the Python 3 section of the docs for
mitigation steps. mitigation steps.
.. note::
In Python 3.7 and later you will no longer get a ``RuntimeError`` in
many cases thanks to :pep:`538` and :pep:`540`, which changed the
default assumption in unconfigured environments.
You are dealing with an environment where Python 3 thinks you are You are dealing with an environment where Python 3 thinks you are
restricted to ASCII data. The solution to these problems is different restricted to ASCII data. The solution to these problems is different
depending on which locale your computer is running in. depending on which locale your computer is running in.
@ -150,15 +153,20 @@ curious about the reasons for this, you can join the discussions in the
Python 3 bug tracker: Python 3 bug tracker:
* `ASCII is a bad filesystem default encoding * `ASCII is a bad filesystem default encoding
<http://bugs.python.org/issue13643#msg149941>`_ <https://bugs.python.org/issue13643#msg149941>`_
* `Use surrogateescape as default error handler * `Use surrogateescape as default error handler
<http://bugs.python.org/issue19977>`_ <https://bugs.python.org/issue19977>`_
* `Python 3 raises Unicode errors in the C locale * `Python 3 raises Unicode errors in the C locale
<http://bugs.python.org/issue19846>`_ <https://bugs.python.org/issue19846>`_
* `LC_CTYPE=C: pydoc leaves terminal in an unusable state * `LC_CTYPE=C: pydoc leaves terminal in an unusable state
<http://bugs.python.org/issue21398>`_ (this is relevant to Click <https://bugs.python.org/issue21398>`_ (this is relevant to Click
because the pager support is provided by the stdlib pydoc module) because the pager support is provided by the stdlib pydoc module)
Note (Python 3.7 onwards): Even though your locale may not be properly
configured, Python 3.7 Click will not raise the above exception because Python
3.7 programs are better at choosing default locales. This doesn't change the
general issue that your locale may be misconfigured.
Unicode Literals Unicode Literals
---------------- ----------------

View file

@ -39,7 +39,7 @@ commands will work for you::
or even better:: or even better::
$ sudo pip install virtualenv $ pip install virtualenv --user
One of these will probably install virtualenv on your system. Maybe it's even One of these will probably install virtualenv on your system. Maybe it's even
in your package manager. If you use Ubuntu, try:: in your package manager. If you use Ubuntu, try::
@ -51,7 +51,7 @@ If you are on Windows (or none of the above methods worked) you must install
Once you have it installed, run the ``pip`` command from above, but without Once you have it installed, run the ``pip`` command from above, but without
the `sudo` prefix. the `sudo` prefix.
.. _installing pip: https://pip.readthedocs.io/en/latest/installing.html .. _installing pip: https://pip.readthedocs.io/en/latest/installing/
Once you have virtualenv installed, just fire up a shell and create Once you have virtualenv installed, just fire up a shell and create
your own environment. I usually create a project folder and a `venv` your own environment. I usually create a project folder and a `venv`

View file

@ -1,2 +1,4 @@
Sphinx~=1.8.0 Sphinx~=2.4.4
Pallets-Sphinx-Themes~=1.1.0 Pallets-Sphinx-Themes~=1.2.3
sphinxcontrib-log-cabinet~=1.0.1
sphinx-issues~=1.2.0

View file

@ -20,48 +20,61 @@ The basic functionality for testing Click applications is the
and captures the output as both bytes and binary data. and captures the output as both bytes and binary data.
The return value is a :class:`Result` object, which has the captured output The return value is a :class:`Result` object, which has the captured output
data, exit code, and optional exception attached. data, exit code, and optional exception attached:
Example:: .. code-block:: python
:caption: hello.py
import click import click
from click.testing import CliRunner
def test_hello_world(): @click.command()
@click.command() @click.argument('name')
@click.argument('name') def hello(name):
def hello(name): click.echo('Hello %s!' % name)
click.echo('Hello %s!' % name)
runner = CliRunner() .. code-block:: python
result = runner.invoke(hello, ['Peter']) :caption: test_hello.py
assert result.exit_code == 0
assert result.output == 'Hello Peter!\n'
For subcommand testing, a subcommand name must be specified in the `args` parameter of :meth:`CliRunner.invoke` method. from click.testing import CliRunner
from hello import hello
Example:: def test_hello_world():
runner = CliRunner()
result = runner.invoke(hello, ['Peter'])
assert result.exit_code == 0
assert result.output == 'Hello Peter!\n'
import click For subcommand testing, a subcommand name must be specified in the `args` parameter of :meth:`CliRunner.invoke` method:
from click.testing import CliRunner
def test_sync():
@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
click.echo('Debug mode is %s' % ('on' if debug else 'off'))
@cli.command()
def sync():
click.echo('Syncing')
runner = CliRunner()
result = runner.invoke(cli, ['--debug', 'sync'])
assert result.exit_code == 0
assert 'Debug mode is on' in result.output
assert 'Syncing' in result.output
Additional keyword arguments passed to ``.invoke()`` will be used to construct the initial Context object. For example, if you want to run your tests against a fixed terminal width you can use the following:: .. code-block:: python
:caption: sync.py
import click
@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
click.echo('Debug mode is %s' % ('on' if debug else 'off'))
@cli.command()
def sync():
click.echo('Syncing')
.. code-block:: python
:caption: test_sync.py
from click.testing import CliRunner
from sync import cli
def test_sync():
runner = CliRunner()
result = runner.invoke(cli, ['--debug', 'sync'])
assert result.exit_code == 0
assert 'Debug mode is on' in result.output
assert 'Syncing' in result.output
Additional keyword arguments passed to ``.invoke()`` will be used to construct the initial Context object.
For example, if you want to run your tests against a fixed terminal width you can use the following::
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ['--debug', 'sync'], terminal_width=60) result = runner.invoke(cli, ['--debug', 'sync'], terminal_width=60)
@ -69,49 +82,63 @@ Additional keyword arguments passed to ``.invoke()`` will be used to construct t
File System Isolation File System Isolation
--------------------- ---------------------
For basic command line tools that want to operate with the file system, the For basic command line tools with file system operations, the
:meth:`CliRunner.isolated_filesystem` method comes in useful which sets up :meth:`CliRunner.isolated_filesystem` method is useful for setting the
an empty folder and changes the current working directory to. current working directory to a new, empty folder.
Example:: .. code-block:: python
:caption: cat.py
import click import click
from click.testing import CliRunner
def test_cat(): @click.command()
@click.command() @click.argument('f', type=click.File())
@click.argument('f', type=click.File()) def cat(f):
def cat(f): click.echo(f.read())
click.echo(f.read())
runner = CliRunner() .. code-block:: python
with runner.isolated_filesystem(): :caption: test_cat.py
with open('hello.txt', 'w') as f:
f.write('Hello World!')
result = runner.invoke(cat, ['hello.txt']) from click.testing import CliRunner
assert result.exit_code == 0 from cat import cat
assert result.output == 'Hello World!\n'
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'])
assert result.exit_code == 0
assert result.output == 'Hello World!\n'
Input Streams Input Streams
------------- -------------
The test wrapper can also be used to provide input data for the input The test wrapper can also be used to provide input data for the input
stream (stdin). This is very useful for testing prompts, for instance:: stream (stdin). This is very useful for testing prompts, for instance:
import click .. code-block:: python
from click.testing import CliRunner :caption: prompt.py
def test_prompts(): import click
@click.command()
@click.option('--foo', prompt=True)
def test(foo):
click.echo('foo=%s' % foo)
runner = CliRunner() @click.command()
result = runner.invoke(test, input='wau wau\n') @click.option('--foo', prompt=True)
assert not result.exception def prompt(foo):
assert result.output == 'Foo: wau wau\nfoo=wau wau\n' click.echo('foo=%s' % foo)
.. code-block:: python
:caption: test_prompt.py
from click.testing import CliRunner
from prompt import prompt
def test_prompts():
runner = CliRunner()
result = runner.invoke(prompt, input='wau wau\n')
assert not result.exception
assert result.output == 'Foo: wau wau\nfoo=wau wau\n'
Note that prompts will be emulated so that they write the input data to Note that prompts will be emulated so that they write the input data to
the output stream as well. If hidden input is expected then this the output stream as well. If hidden input is expected then this

View file

@ -6,6 +6,37 @@ this is not entirely possible. In case we need to break backwards
compatibility this document gives you information about how to upgrade or compatibility this document gives you information about how to upgrade or
handle backwards compatibility properly. handle backwards compatibility properly.
.. _upgrade-to-7.0:
Upgrading to 7.0
----------------
Commands that take their name from the decorated function now replace
underscores with dashes. For example, the Python function ``run_server``
will get the command name ``run-server`` now. There are a few options
to address this:
- To continue with the new behavior, pin your dependency to
``Click>=7`` and update any documentation to use dashes.
- To keep existing behavior, add an explicit command name with
underscores, like ``@click.command("run_server")``.
- To try a name with dashes if the name with underscores was not
found, pass a ``token_normalize_func`` to the context:
.. code-block:: python
def normalize(name):
return name.replace("_", "-")
@click.group(context_settings={"token_normalize_func": normalize})
def group():
...
@group.command()
def run_server():
...
.. _upgrade-to-3.2: .. _upgrade-to-3.2:
Upgrading to 3.2 Upgrading to 3.2

View file

@ -33,7 +33,7 @@ suppressed by passing ``nl=False``::
Last but not least :func:`echo` uses click's intelligent internal output Last but not least :func:`echo` uses click's intelligent internal output
streams to stdout and stderr which support unicode output on the Windows streams to stdout and stderr which support unicode output on the Windows
console. This means for as long as you are using `click.echo` you can console. This means for as long as you are using `click.echo` you can
output unicode character (there are some limitations on the default font output unicode characters (there are some limitations on the default font
with regards to which characters can be displayed). This functionality is with regards to which characters can be displayed). This functionality is
new in Click 6.0. new in Click 6.0.
@ -211,7 +211,7 @@ Click supports launching editors automatically through :func:`edit`. This
is very useful for asking users for multi-line input. It will is very useful for asking users for multi-line input. It will
automatically open the user's defined editor or fall back to a sensible automatically open the user's defined editor or fall back to a sensible
default. If the user closes the editor without saving, the return value default. If the user closes the editor without saving, the return value
will be `None` otherwise the entered text. will be ``None``, otherwise the entered text.
Example usage:: Example usage::
@ -389,6 +389,16 @@ but you know the length, you can explicitly provide it::
for user in bar: for user in bar:
modify_the_user(user) modify_the_user(user)
Note that :func:`progressbar` updates the bar *after* each iteration of the
loop. So code like this will render correctly::
import time
with click.progressbar([1, 2, 3]) as bar:
for x in bar:
print('sleep({})...'.format(x))
time.sleep(x)
Another useful feature is to associate a label with the progress bar which Another useful feature is to associate a label with the progress bar which
will be shown preceding the progress bar:: will be shown preceding the progress bar::

View file

@ -10,7 +10,7 @@ line utility for Python out there which ticks the following boxes:
* is lazily composable without restrictions * is lazily composable without restrictions
* supports implementation of Unix/POSIX command line conventions * supports implementation of Unix/POSIX command line conventions
* supports loading values from environment variables out of the box * supports loading values from environment variables out of the box
* supports for prompting of custom values * support for prompting of custom values
* is fully nestable and composable * is fully nestable and composable
* works the same in Python 2 and 3 * works the same in Python 2 and 3
* supports file handling out of the box * supports file handling out of the box
@ -18,56 +18,51 @@ line utility for Python out there which ticks the following boxes:
ANSI colors, fetching direct keyboard input, screen clearing, ANSI colors, fetching direct keyboard input, screen clearing,
finding config paths, launching apps and editors, etc.) finding config paths, launching apps and editors, etc.)
There are many alternatives to Click and you can have a look at them if There are many alternatives to Click; the obvious ones are ``optparse``
you enjoy them better. The obvious ones are ``optparse`` and ``argparse`` and ``argparse`` from the standard library. Have a look to see if something
from the standard library. else resonates with you.
Click is actually implemented as a wrapper around a mild fork of Click actually implements its own parsing of arguments and does not use
``optparse`` and does not implement any parsing itself. The reason it's ``optparse`` or ``argparse`` following the ``optparse`` parsing behavior.
not based on ``argparse`` is that ``argparse`` does not allow proper The reason it's not based on ``argparse`` is that ``argparse`` does not
nesting of commands by design and has some deficiencies when it comes to allow proper nesting of commands by design and has some deficiencies when
POSIX compliant argument handling. it comes to POSIX compliant argument handling.
Click is designed to be fun to work with and at the same time not stand in Click is designed to be fun and customizable but not overly flexible.
your way. It's not overly flexible either. Currently, for instance, it For instance, the customizability of help pages is constrained. This
does not allow you to customize the help pages too much. This is intentional constraint is intentional because Click promises multiple Click instances
because Click is designed to allow you to nest command line utilities. The will continue to function as intended when strung together.
idea is that you can have a system that works together with another system by
tacking two Click instances together and they will continue working as they
should.
Too much customizability would break this promise. Too much customizability would break this promise.
Click was written to support the `Flask <http://flask.pocoo.org/>`_ Click was written to support the `Flask <https://palletsprojects.com/p/flask/>`_
microframework ecosystem because no tool could provide it with the microframework ecosystem because no tool could provide it with the
functionality it needed. functionality it needed.
To get an understanding of what Click is all about, I strongly recommend To get an understanding of what Click is all about, I strongly recommend
looking at the :ref:`complex-guide` chapter to see what it's useful for. looking at the :ref:`complex-guide` chapter.
Why not Argparse? Why not Argparse?
----------------- -----------------
Click is internally based on optparse instead of argparse. This however Click is internally based on ``optparse`` instead of ``argparse``. This
is an implementation detail that a user does not have to be concerned is an implementation detail that a user does not have to be concerned
with. The reason however Click is not using argparse is that it has some with. Click is not based on argparse because it has some behaviors that
problematic behaviors that make handling arbitrary command line interfaces make handling arbitrary command line interfaces hard:
hard:
* argparse has built-in magic behavior to guess if something is an * argparse has built-in behavior to guess if something is an
argument or an option. This becomes a problem when dealing with argument or an option. This becomes a problem when dealing with
incomplete command lines as it's not possible to know without having a incomplete command lines; the behaviour becomes unpredictable
full understanding of the command line how the parser is going to without full knowledge of a command line. This goes against Click's
behave. This goes against Click's ambitions of dispatching to ambitions of dispatching to subparsers.
subparsers. * argparse does not support disabling interspersed arguments. Without
* argparse currently does not support disabling of interspersed this feature, it's not possible to safely implement Click's nested
arguments. Without this feature it's not possible to safely implement parsing.
Click's nested parsing nature.
Why not Docopt etc.? Why not Docopt etc.?
-------------------- --------------------
Docopt and many tools like it are cool in how they work, but very few of Docopt, and many tools like it, are cool in how they work, but very few of
these tools deal with nesting of commands and composability in a way like these tools deal with nesting of commands and composability in a way like
Click. To the best of the developer's knowledge, Click is the first Click. To the best of the developer's knowledge, Click is the first
Python library that aims to create a level of composability of applications Python library that aims to create a level of composability of applications
@ -78,13 +73,13 @@ according to those rules. The side effect of this is that docopt is quite
rigid in how it handles the command line interface. The upside of docopt rigid in how it handles the command line interface. The upside of docopt
is that it gives you strong control over your help page; the downside is is that it gives you strong control over your help page; the downside is
that due to this it cannot rewrap your output for the current terminal that due to this it cannot rewrap your output for the current terminal
width and it makes translations hard. On top of that docopt is restricted width, and it makes translations hard. On top of that, docopt is restricted
to basic parsing. It does not handle argument dispatching and callback to basic parsing. It does not handle argument dispatching and callback
invocation or types. This means there is a lot of code that needs to be invocation or types. This means there is a lot of code that needs to be
written in addition to the basic help page to handle the parsing results. written in addition to the basic help page to handle the parsing results.
Most of all, however, it makes composability hard. While docopt does Most of all, however, it makes composability hard. While docopt does
support dispatching to subcommands, it for instance does not directly support dispatching to subcommands, it, for instance, does not directly
support any kind of automatic subcommand enumeration based on what's support any kind of automatic subcommand enumeration based on what's
available or it does not enforce subcommands to work in a consistent way. available or it does not enforce subcommands to work in a consistent way.
@ -95,25 +90,25 @@ following:
- Click does not just parse, it also dispatches to the appropriate code. - Click does not just parse, it also dispatches to the appropriate code.
- Click has a strong concept of an invocation context that allows - Click has a strong concept of an invocation context that allows
subcommands to respond to data from the parent command. subcommands to respond to data from the parent command.
- Click has strong information available for all parameters and commands - Click has strong information available for all parameters and commands,
so that it can generate unified help pages for the full CLI and to so it can generate unified help pages for the full CLI and
assist the user in converting the input data as necessary. assist the user in converting the input data as necessary.
- Click has a strong understanding of what types are and can give the user - Click has a strong understanding of what types are, and it can give the user
consistent error messages if something goes wrong. A subcommand consistent error messages if something goes wrong. A subcommand
written by a different developer will not suddenly die with a written by a different developer will not suddenly die with a
different error messsage because it's manually handled. different error message because it's manually handled.
- Click has enough meta information available for its whole program - Click has enough meta information available for its whole program
that it can evolve over time to improve the user experience without to evolve over time and improve the user experience without
forcing developers to adjust their programs. For instance, if Click forcing developers to adjust their programs. For instance, if Click
decides to change how help pages are formatted, all Click programs decides to change how help pages are formatted, all Click programs
will automatically benefit from this. will automatically benefit from this.
The aim of Click is to make composable systems, whereas the aim of docopt The aim of Click is to make composable systems. Whereas, the aim of docopt
is to build the most beautiful and hand-crafted command line interfaces. is to build the most beautiful and hand-crafted command line interfaces.
These two goals conflict with one another in subtle ways. Click These two goals conflict with one another in subtle ways. Click
actively prevents people from implementing certain patterns in order to actively prevents people from implementing certain patterns in order to
achieve unified command line interfaces. You have very little input on achieve unified command line interfaces. For instance, as a developer, you
reformatting your help pages for instance. are given very little choice in formatting your help pages.
Why Hardcoded Behaviors? Why Hardcoded Behaviors?
@ -125,7 +120,7 @@ reasons for this. The biggest one is that too much configurability makes
it hard to achieve a consistent command line experience. it hard to achieve a consistent command line experience.
The best example for this is optparse's ``callback`` functionality for The best example for this is optparse's ``callback`` functionality for
accepting arbitrary number of arguments. Due to syntactical ambiguities accepting an arbitrary number of arguments. Due to syntactical ambiguities
on the command line, there is no way to implement fully variadic arguments. on the command line, there is no way to implement fully variadic arguments.
There are always tradeoffs that need to be made and in case of There are always tradeoffs that need to be made and in case of
``argparse`` these tradeoffs have been critical enough, that a system like ``argparse`` these tradeoffs have been critical enough, that a system like
@ -144,7 +139,7 @@ even optparse and argparse support automatic expansion of long arguments.
The reason for this is that it's a liability for backwards compatibility. The reason for this is that it's a liability for backwards compatibility.
If people start relying on automatically modified parameters and someone If people start relying on automatically modified parameters and someone
adds a new parameter in the future, the script might stop working. These adds a new parameter in the future, the script might stop working. These
kinds of problems are hard to find so Click does not attempt to be magical kinds of problems are hard to find, so Click does not attempt to be magical
about this. about this.
This sort of behavior however can be implemented on a higher level to This sort of behavior however can be implemented on a higher level to

View file

@ -1,10 +1,11 @@
import os import os
import click import click
try: try:
import ConfigParser as configparser
except ImportError:
import configparser import configparser
except ImportError:
import ConfigParser as configparser
class Config(object): class Config(object):
@ -14,14 +15,25 @@ class Config(object):
self.path = os.getcwd() self.path = os.getcwd()
self.aliases = {} self.aliases = {}
def add_alias(self, alias, cmd):
self.aliases.update({alias: cmd})
def read_config(self, filename): def read_config(self, filename):
parser = configparser.RawConfigParser() parser = configparser.RawConfigParser()
parser.read([filename]) parser.read([filename])
try: try:
self.aliases.update(parser.items('aliases')) self.aliases.update(parser.items("aliases"))
except configparser.NoSectionError: except configparser.NoSectionError:
pass pass
def write_config(self, filename):
parser = configparser.RawConfigParser()
parser.add_section("aliases")
for key, value in self.aliases.items():
parser.set("aliases", key, value)
with open(filename, "wb") as file:
parser.write(file)
pass_config = click.make_pass_decorator(Config, ensure=True) pass_config = click.make_pass_decorator(Config, ensure=True)
@ -50,13 +62,14 @@ class AliasedGroup(click.Group):
# allow automatic abbreviation of the command. "status" for # allow automatic abbreviation of the command. "status" for
# instance will match "st". We only allow that however if # instance will match "st". We only allow that however if
# there is only one command. # there is only one command.
matches = [x for x in self.list_commands(ctx) matches = [
if x.lower().startswith(cmd_name.lower())] x for x in self.list_commands(ctx) if x.lower().startswith(cmd_name.lower())
]
if not matches: if not matches:
return None return None
elif len(matches) == 1: elif len(matches) == 1:
return click.Group.get_command(self, ctx, matches[0]) return click.Group.get_command(self, ctx, matches[0])
ctx.fail('Too many matches: %s' % ', '.join(sorted(matches))) ctx.fail("Too many matches: {}".format(", ".join(sorted(matches))))
def read_config(ctx, param, value): def read_config(ctx, param, value):
@ -67,15 +80,19 @@ def read_config(ctx, param, value):
""" """
cfg = ctx.ensure_object(Config) cfg = ctx.ensure_object(Config)
if value is None: if value is None:
value = os.path.join(os.path.dirname(__file__), 'aliases.ini') value = os.path.join(os.path.dirname(__file__), "aliases.ini")
cfg.read_config(value) cfg.read_config(value)
return value return value
@click.command(cls=AliasedGroup) @click.command(cls=AliasedGroup)
@click.option('--config', type=click.Path(exists=True, dir_okay=False), @click.option(
callback=read_config, expose_value=False, "--config",
help='The config file to use instead of the default.') type=click.Path(exists=True, dir_okay=False),
callback=read_config,
expose_value=False,
help="The config file to use instead of the default.",
)
def cli(): def cli():
"""An example application that supports aliases.""" """An example application that supports aliases."""
@ -83,29 +100,43 @@ def cli():
@cli.command() @cli.command()
def push(): def push():
"""Pushes changes.""" """Pushes changes."""
click.echo('Push') click.echo("Push")
@cli.command() @cli.command()
def pull(): def pull():
"""Pulls changes.""" """Pulls changes."""
click.echo('Pull') click.echo("Pull")
@cli.command() @cli.command()
def clone(): def clone():
"""Clones a repository.""" """Clones a repository."""
click.echo('Clone') click.echo("Clone")
@cli.command() @cli.command()
def commit(): def commit():
"""Commits pending changes.""" """Commits pending changes."""
click.echo('Commit') click.echo("Commit")
@cli.command() @cli.command()
@pass_config @pass_config
def status(config): def status(config):
"""Shows the status.""" """Shows the status."""
click.echo('Status for %s' % config.path) click.echo("Status for {}".format(config.path))
@cli.command()
@pass_config
@click.argument("alias_", metavar="ALIAS", type=click.STRING)
@click.argument("cmd", type=click.STRING)
@click.option(
"--config_file", type=click.Path(exists=True, dir_okay=False), default="aliases.ini"
)
def alias(config, alias_, cmd, config_file):
"""Adds an alias to the specified configuration file."""
config.add_alias(alias_, cmd)
config.write_config(config_file)
click.echo("Added '{}' as alias for '{}'".format(alias_, cmd))

View file

@ -1,15 +1,13 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-aliases', name="click-example-aliases",
version='1.0', version="1.0",
py_modules=['aliases'], py_modules=["aliases"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
'click', entry_points="""
],
entry_points='''
[console_scripts] [console_scripts]
aliases=aliases:cli aliases=aliases:cli
''', """,
) )

View file

@ -1,6 +1,7 @@
import click
import os import os
import click
@click.group() @click.group()
def cli(): def cli():
@ -14,14 +15,14 @@ def get_env_vars(ctx, args, incomplete):
yield key yield key
@cli.command(help='A command to print environment variables') @cli.command(help="A command to print environment variables")
@click.argument("envvar", type=click.STRING, autocompletion=get_env_vars) @click.argument("envvar", type=click.STRING, autocompletion=get_env_vars)
def cmd1(envvar): def cmd1(envvar):
click.echo('Environment variable: %s' % envvar) click.echo("Environment variable: {}".format(envvar))
click.echo('Value: %s' % os.environ[envvar]) click.echo("Value: {}".format(os.environ[envvar]))
@click.group(help='A group that holds a subcommand') @click.group(help="A group that holds a subcommand")
def group(): def group():
pass pass
@ -29,17 +30,16 @@ def group():
def list_users(ctx, args, incomplete): def list_users(ctx, args, incomplete):
# You can generate completions with descriptions by returning # You can generate completions with descriptions by returning
# tuples in the form (completion, description). # tuples in the form (completion, description).
users = [('bob', 'butcher'), users = [("bob", "butcher"), ("alice", "baker"), ("jerry", "candlestick maker")]
('alice', 'baker'), # Ths will allow completion matches based on matches within the
('jerry', 'candlestick maker')] # description string too!
# Ths will allow completion matches based on matches within the description string too!
return [user for user in users if incomplete in user[0] or incomplete in user[1]] return [user for user in users if incomplete in user[0] or incomplete in user[1]]
@group.command(help='Choose a user') @group.command(help="Choose a user")
@click.argument("user", type=click.STRING, autocompletion=list_users) @click.argument("user", type=click.STRING, autocompletion=list_users)
def subcmd(user): def subcmd(user):
click.echo('Chosen user is %s' % user) click.echo("Chosen user is {}".format(user))
cli.add_command(group) cli.add_command(group)

View file

@ -1,15 +1,13 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-bashcompletion', name="click-example-bashcompletion",
version='1.0', version="1.0",
py_modules=['bashcompletion'], py_modules=["bashcompletion"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
'click', entry_points="""
],
entry_points='''
[console_scripts] [console_scripts]
bashcompletion=bashcompletion:cli bashcompletion=bashcompletion:cli
''', """,
) )

View file

@ -1,10 +1,24 @@
import click import click
all_colors = 'black', 'red', 'green', 'yellow', 'blue', 'magenta', \ all_colors = (
'cyan', 'white', 'bright_black', 'bright_red', \ "black",
'bright_green', 'bright_yellow', 'bright_blue', \ "red",
'bright_magenta', 'bright_cyan', 'bright_white' "green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"bright_black",
"bright_red",
"bright_green",
"bright_yellow",
"bright_blue",
"bright_magenta",
"bright_cyan",
"bright_white",
)
@click.command() @click.command()
@ -16,13 +30,15 @@ def cli():
Give it a try! Give it a try!
""" """
for color in all_colors: for color in all_colors:
click.echo(click.style('I am colored %s' % color, fg=color)) click.echo(click.style("I am colored {}".format(color), fg=color))
for color in all_colors: for color in all_colors:
click.echo(click.style('I am colored %s and bold' % color, click.echo(
fg=color, bold=True)) click.style("I am colored {} and bold".format(color), fg=color, bold=True)
)
for color in all_colors: for color in all_colors:
click.echo(click.style('I am reverse colored %s' % color, fg=color, click.echo(
reverse=True)) click.style("I am reverse colored {}".format(color), fg=color, reverse=True)
)
click.echo(click.style('I am blinking', blink=True)) click.echo(click.style("I am blinking", blink=True))
click.echo(click.style('I am underlined', underline=True)) click.echo(click.style("I am underlined", underline=True))

View file

@ -1,17 +1,17 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-colors', name="click-example-colors",
version='1.0', version="1.0",
py_modules=['colors'], py_modules=["colors"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'click', "click",
# Colorama is only required for Windows. # Colorama is only required for Windows.
'colorama', "colorama",
], ],
entry_points=''' entry_points="""
[console_scripts] [console_scripts]
colors=colors:cli colors=colors:cli
''', """,
) )

View file

@ -1,13 +1,13 @@
import os import os
import sys import sys
import click import click
CONTEXT_SETTINGS = dict(auto_envvar_prefix='COMPLEX') CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX")
class Context(object): class Environment(object):
def __init__(self): def __init__(self):
self.verbose = False self.verbose = False
self.home = os.getcwd() self.home = os.getcwd()
@ -24,18 +24,15 @@ class Context(object):
self.log(msg, *args) self.log(msg, *args)
pass_context = click.make_pass_decorator(Context, ensure=True) pass_environment = click.make_pass_decorator(Environment, ensure=True)
cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "commands"))
'commands'))
class ComplexCLI(click.MultiCommand): class ComplexCLI(click.MultiCommand):
def list_commands(self, ctx): def list_commands(self, ctx):
rv = [] rv = []
for filename in os.listdir(cmd_folder): for filename in os.listdir(cmd_folder):
if filename.endswith('.py') and \ if filename.endswith(".py") and filename.startswith("cmd_"):
filename.startswith('cmd_'):
rv.append(filename[4:-3]) rv.append(filename[4:-3])
rv.sort() rv.sort()
return rv return rv
@ -43,21 +40,23 @@ class ComplexCLI(click.MultiCommand):
def get_command(self, ctx, name): def get_command(self, ctx, name):
try: try:
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
name = name.encode('ascii', 'replace') name = name.encode("ascii", "replace")
mod = __import__('complex.commands.cmd_' + name, mod = __import__(
None, None, ['cli']) "complex.commands.cmd_{}".format(name), None, None, ["cli"]
)
except ImportError: except ImportError:
return return
return mod.cli return mod.cli
@click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS) @click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS)
@click.option('--home', type=click.Path(exists=True, file_okay=False, @click.option(
resolve_path=True), "--home",
help='Changes the folder to operate on.') type=click.Path(exists=True, file_okay=False, resolve_path=True),
@click.option('-v', '--verbose', is_flag=True, help="Changes the folder to operate on.",
help='Enables verbose mode.') )
@pass_context @click.option("-v", "--verbose", is_flag=True, help="Enables verbose mode.")
@pass_environment
def cli(ctx, verbose, home): def cli(ctx, verbose, home):
"""A complex command line interface.""" """A complex command line interface."""
ctx.verbose = verbose ctx.verbose = verbose

View file

@ -1,13 +1,13 @@
from complex.cli import pass_environment
import click import click
from complex.cli import pass_context
@click.command('init', short_help='Initializes a repo.') @click.command("init", short_help="Initializes a repo.")
@click.argument('path', required=False, type=click.Path(resolve_path=True)) @click.argument("path", required=False, type=click.Path(resolve_path=True))
@pass_context @pass_environment
def cli(ctx, path): def cli(ctx, path):
"""Initializes a repository.""" """Initializes a repository."""
if path is None: if path is None:
path = ctx.home path = ctx.home
ctx.log('Initialized the repository in %s', ctx.log("Initialized the repository in %s", click.format_filename(path))
click.format_filename(path))

View file

@ -1,10 +1,11 @@
from complex.cli import pass_environment
import click import click
from complex.cli import pass_context
@click.command('status', short_help='Shows file changes.') @click.command("status", short_help="Shows file changes.")
@pass_context @pass_environment
def cli(ctx): def cli(ctx):
"""Shows file changes in the current working directory.""" """Shows file changes in the current working directory."""
ctx.log('Changed files: none') ctx.log("Changed files: none")
ctx.vlog('bla bla bla, debug info') ctx.vlog("bla bla bla, debug info")

View file

@ -1,15 +1,13 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-complex', name="click-example-complex",
version='1.0', version="1.0",
packages=['complex', 'complex.commands'], packages=["complex", "complex.commands"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
'click', entry_points="""
],
entry_points='''
[console_scripts] [console_scripts]
complex=complex.cli:cli complex=complex.cli:cli
''', """,
) )

View file

@ -1,6 +1,10 @@
import click
from functools import update_wrapper from functools import update_wrapper
from PIL import Image, ImageFilter, ImageEnhance
from PIL import Image
from PIL import ImageEnhance
from PIL import ImageFilter
import click
@click.group(chain=True) @click.group(chain=True)
@ -39,10 +43,13 @@ def processor(f):
"""Helper decorator to rewrite a function so that it returns another """Helper decorator to rewrite a function so that it returns another
function from it. function from it.
""" """
def new_func(*args, **kwargs): def new_func(*args, **kwargs):
def processor(stream): def processor(stream):
return f(stream, *args, **kwargs) return f(stream, *args, **kwargs)
return processor return processor
return update_wrapper(new_func, f) return update_wrapper(new_func, f)
@ -50,12 +57,14 @@ def generator(f):
"""Similar to the :func:`processor` but passes through old values """Similar to the :func:`processor` but passes through old values
unchanged and does not pass through the values as parameter. unchanged and does not pass through the values as parameter.
""" """
@processor @processor
def new_func(stream, *args, **kwargs): def new_func(stream, *args, **kwargs):
for item in stream: for item in stream:
yield item yield item
for item in f(*args, **kwargs): for item in f(*args, **kwargs):
yield item yield item
return update_wrapper(new_func, f) return update_wrapper(new_func, f)
@ -64,9 +73,15 @@ def copy_filename(new, old):
return new return new
@cli.command('open') @cli.command("open")
@click.option('-i', '--image', 'images', type=click.Path(), @click.option(
multiple=True, help='The image file to open.') "-i",
"--image",
"images",
type=click.Path(),
multiple=True,
help="The image file to open.",
)
@generator @generator
def open_cmd(images): def open_cmd(images):
"""Loads one or multiple images for processing. The input parameter """Loads one or multiple images for processing. The input parameter
@ -74,47 +89,52 @@ def open_cmd(images):
""" """
for image in images: for image in images:
try: try:
click.echo('Opening "%s"' % image) click.echo("Opening '{}'".format(image))
if image == '-': if image == "-":
img = Image.open(click.get_binary_stdin()) img = Image.open(click.get_binary_stdin())
img.filename = '-' img.filename = "-"
else: else:
img = Image.open(image) img = Image.open(image)
yield img yield img
except Exception as e: except Exception as e:
click.echo('Could not open image "%s": %s' % (image, e), err=True) click.echo("Could not open image '{}': {}".format(image, e), err=True)
@cli.command('save') @cli.command("save")
@click.option('--filename', default='processed-%04d.png', type=click.Path(), @click.option(
help='The format for the filename.', "--filename",
show_default=True) default="processed-{:04}.png",
type=click.Path(),
help="The format for the filename.",
show_default=True,
)
@processor @processor
def save_cmd(images, filename): def save_cmd(images, filename):
"""Saves all processed images to a series of files.""" """Saves all processed images to a series of files."""
for idx, image in enumerate(images): for idx, image in enumerate(images):
try: try:
fn = filename % (idx + 1) fn = filename.format(idx + 1)
click.echo('Saving "%s" as "%s"' % (image.filename, fn)) click.echo("Saving '{}' as '{}'".format(image.filename, fn))
yield image.save(fn) yield image.save(fn)
except Exception as e: except Exception as e:
click.echo('Could not save image "%s": %s' % click.echo(
(image.filename, e), err=True) "Could not save image '{}': {}".format(image.filename, e), err=True
)
@cli.command('display') @cli.command("display")
@processor @processor
def display_cmd(images): def display_cmd(images):
"""Opens all images in an image viewer.""" """Opens all images in an image viewer."""
for image in images: for image in images:
click.echo('Displaying "%s"' % image.filename) click.echo("Displaying '{}'".format(image.filename))
image.show() image.show()
yield image yield image
@cli.command('resize') @cli.command("resize")
@click.option('-w', '--width', type=int, help='The new width of the image.') @click.option("-w", "--width", type=int, help="The new width of the image.")
@click.option('-h', '--height', type=int, help='The new height of the image.') @click.option("-h", "--height", type=int, help="The new height of the image.")
@processor @processor
def resize_cmd(images, width, height): def resize_cmd(images, width, height):
"""Resizes an image by fitting it into the box without changing """Resizes an image by fitting it into the box without changing
@ -122,14 +142,15 @@ def resize_cmd(images, width, height):
""" """
for image in images: for image in images:
w, h = (width or image.size[0], height or image.size[1]) w, h = (width or image.size[0], height or image.size[1])
click.echo('Resizing "%s" to %dx%d' % (image.filename, w, h)) click.echo("Resizing '{}' to {}x{}".format(image.filename, w, h))
image.thumbnail((w, h)) image.thumbnail((w, h))
yield image yield image
@cli.command('crop') @cli.command("crop")
@click.option('-b', '--border', type=int, help='Crop the image from all ' @click.option(
'sides by this amount.') "-b", "--border", type=int, help="Crop the image from all sides by this amount."
)
@processor @processor
def crop_cmd(images, border): def crop_cmd(images, border):
"""Crops an image from all edges.""" """Crops an image from all edges."""
@ -139,7 +160,7 @@ def crop_cmd(images, border):
if border is not None: if border is not None:
for idx, val in enumerate(box): for idx, val in enumerate(box):
box[idx] = max(0, val - border) box[idx] = max(0, val - border)
click.echo('Cropping "%s" by %dpx' % (image.filename, border)) click.echo("Cropping '{}' by {}px".format(image.filename, border))
yield copy_filename(image.crop(box), image) yield copy_filename(image.crop(box), image)
else: else:
yield image yield image
@ -149,96 +170,104 @@ def convert_rotation(ctx, param, value):
if value is None: if value is None:
return return
value = value.lower() value = value.lower()
if value in ('90', 'r', 'right'): if value in ("90", "r", "right"):
return (Image.ROTATE_90, 90) return (Image.ROTATE_90, 90)
if value in ('180', '-180'): if value in ("180", "-180"):
return (Image.ROTATE_180, 180) return (Image.ROTATE_180, 180)
if value in ('-90', '270', 'l', 'left'): if value in ("-90", "270", "l", "left"):
return (Image.ROTATE_270, 270) return (Image.ROTATE_270, 270)
raise click.BadParameter('invalid rotation "%s"' % value) raise click.BadParameter("invalid rotation '{}'".format(value))
def convert_flip(ctx, param, value): def convert_flip(ctx, param, value):
if value is None: if value is None:
return return
value = value.lower() value = value.lower()
if value in ('lr', 'leftright'): if value in ("lr", "leftright"):
return (Image.FLIP_LEFT_RIGHT, 'left to right') return (Image.FLIP_LEFT_RIGHT, "left to right")
if value in ('tb', 'topbottom', 'upsidedown', 'ud'): if value in ("tb", "topbottom", "upsidedown", "ud"):
return (Image.FLIP_LEFT_RIGHT, 'top to bottom') return (Image.FLIP_LEFT_RIGHT, "top to bottom")
raise click.BadParameter('invalid flip "%s"' % value) raise click.BadParameter("invalid flip '{}'".format(value))
@cli.command('transpose') @cli.command("transpose")
@click.option('-r', '--rotate', callback=convert_rotation, @click.option(
help='Rotates the image (in degrees)') "-r", "--rotate", callback=convert_rotation, help="Rotates the image (in degrees)"
@click.option('-f', '--flip', callback=convert_flip, )
help='Flips the image [LR / TB]') @click.option("-f", "--flip", callback=convert_flip, help="Flips the image [LR / TB]")
@processor @processor
def transpose_cmd(images, rotate, flip): def transpose_cmd(images, rotate, flip):
"""Transposes an image by either rotating or flipping it.""" """Transposes an image by either rotating or flipping it."""
for image in images: for image in images:
if rotate is not None: if rotate is not None:
mode, degrees = rotate mode, degrees = rotate
click.echo('Rotate "%s" by %ddeg' % (image.filename, degrees)) click.echo("Rotate '{}' by {}deg".format(image.filename, degrees))
image = copy_filename(image.transpose(mode), image) image = copy_filename(image.transpose(mode), image)
if flip is not None: if flip is not None:
mode, direction = flip mode, direction = flip
click.echo('Flip "%s" %s' % (image.filename, direction)) click.echo("Flip '{}' {}".format(image.filename, direction))
image = copy_filename(image.transpose(mode), image) image = copy_filename(image.transpose(mode), image)
yield image yield image
@cli.command('blur') @cli.command("blur")
@click.option('-r', '--radius', default=2, show_default=True, @click.option("-r", "--radius", default=2, show_default=True, help="The blur radius.")
help='The blur radius.')
@processor @processor
def blur_cmd(images, radius): def blur_cmd(images, radius):
"""Applies gaussian blur.""" """Applies gaussian blur."""
blur = ImageFilter.GaussianBlur(radius) blur = ImageFilter.GaussianBlur(radius)
for image in images: for image in images:
click.echo('Blurring "%s" by %dpx' % (image.filename, radius)) click.echo("Blurring '{}' by {}px".format(image.filename, radius))
yield copy_filename(image.filter(blur), image) yield copy_filename(image.filter(blur), image)
@cli.command('smoothen') @cli.command("smoothen")
@click.option('-i', '--iterations', default=1, show_default=True, @click.option(
help='How many iterations of the smoothen filter to run.') "-i",
"--iterations",
default=1,
show_default=True,
help="How many iterations of the smoothen filter to run.",
)
@processor @processor
def smoothen_cmd(images, iterations): def smoothen_cmd(images, iterations):
"""Applies a smoothening filter.""" """Applies a smoothening filter."""
for image in images: for image in images:
click.echo('Smoothening "%s" %d time%s' % click.echo(
(image.filename, iterations, iterations != 1 and 's' or '',)) "Smoothening '{}' {} time{}".format(
for x in xrange(iterations): image.filename, iterations, "s" if iterations != 1 else ""
)
)
for _ in range(iterations):
image = copy_filename(image.filter(ImageFilter.BLUR), image) image = copy_filename(image.filter(ImageFilter.BLUR), image)
yield image yield image
@cli.command('emboss') @cli.command("emboss")
@processor @processor
def emboss_cmd(images): def emboss_cmd(images):
"""Embosses an image.""" """Embosses an image."""
for image in images: for image in images:
click.echo('Embossing "%s"' % image.filename) click.echo("Embossing '{}'".format(image.filename))
yield copy_filename(image.filter(ImageFilter.EMBOSS), image) yield copy_filename(image.filter(ImageFilter.EMBOSS), image)
@cli.command('sharpen') @cli.command("sharpen")
@click.option('-f', '--factor', default=2.0, @click.option(
help='Sharpens the image.', show_default=True) "-f", "--factor", default=2.0, help="Sharpens the image.", show_default=True
)
@processor @processor
def sharpen_cmd(images, factor): def sharpen_cmd(images, factor):
"""Sharpens an image.""" """Sharpens an image."""
for image in images: for image in images:
click.echo('Sharpen "%s" by %f' % (image.filename, factor)) click.echo("Sharpen '{}' by {}".format(image.filename, factor))
enhancer = ImageEnhance.Sharpness(image) enhancer = ImageEnhance.Sharpness(image)
yield copy_filename(enhancer.enhance(max(1.0, factor)), image) yield copy_filename(enhancer.enhance(max(1.0, factor)), image)
@cli.command('paste') @cli.command("paste")
@click.option('-l', '--left', default=0, help='Offset from left.') @click.option("-l", "--left", default=0, help="Offset from left.")
@click.option('-r', '--right', default=0, help='Offset from right.') @click.option("-r", "--right", default=0, help="Offset from right.")
@processor @processor
def paste_cmd(images, left, right): def paste_cmd(images, left, right):
"""Pastes the second image on the first image and leaves the rest """Pastes the second image on the first image and leaves the rest
@ -253,13 +282,12 @@ def paste_cmd(images, left, right):
yield image yield image
return return
click.echo('Paste "%s" on "%s"' % click.echo("Paste '{}' on '{}'".format(to_paste.filename, image.filename))
(to_paste.filename, image.filename))
mask = None mask = None
if to_paste.mode == 'RGBA' or 'transparency' in to_paste.info: if to_paste.mode == "RGBA" or "transparency" in to_paste.info:
mask = to_paste mask = to_paste
image.paste(to_paste, (left, right), mask) image.paste(to_paste, (left, right), mask)
image.filename += '+' + to_paste.filename image.filename += "+{}".format(to_paste.filename)
yield image yield image
for image in imageiter: for image in imageiter:

View file

@ -1,16 +1,13 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-imagepipe', name="click-example-imagepipe",
version='1.0', version="1.0",
py_modules=['imagepipe'], py_modules=["imagepipe"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click", "pillow"],
'click', entry_points="""
'pillow',
],
entry_points='''
[console_scripts] [console_scripts]
imagepipe=imagepipe:cli imagepipe=imagepipe:cli
''', """,
) )

View file

@ -2,8 +2,8 @@ import click
@click.command() @click.command()
@click.argument('input', type=click.File('rb'), nargs=-1) @click.argument("input", type=click.File("rb"), nargs=-1)
@click.argument('output', type=click.File('wb')) @click.argument("output", type=click.File("wb"))
def cli(input, output): def cli(input, output):
"""This script works similar to the Unix `cat` command but it writes """This script works similar to the Unix `cat` command but it writes
into a specific file (which could be the standard output as denoted by into a specific file (which could be the standard output as denoted by

View file

@ -1,15 +1,13 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-inout', name="click-example-inout",
version='0.1', version="0.1",
py_modules=['inout'], py_modules=["inout"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
'click', entry_points="""
],
entry_points='''
[console_scripts] [console_scripts]
inout=inout:cli inout=inout:cli
''', """,
) )

View file

@ -17,54 +17,56 @@ def ship():
"""Manages ships.""" """Manages ships."""
@ship.command('new') @ship.command("new")
@click.argument('name') @click.argument("name")
def ship_new(name): def ship_new(name):
"""Creates a new ship.""" """Creates a new ship."""
click.echo('Created ship %s' % name) click.echo("Created ship {}".format(name))
@ship.command('move') @ship.command("move")
@click.argument('ship') @click.argument("ship")
@click.argument('x', type=float) @click.argument("x", type=float)
@click.argument('y', type=float) @click.argument("y", type=float)
@click.option('--speed', metavar='KN', default=10, @click.option("--speed", metavar="KN", default=10, help="Speed in knots.")
help='Speed in knots.')
def ship_move(ship, x, y, speed): def ship_move(ship, x, y, speed):
"""Moves SHIP to the new location X,Y.""" """Moves SHIP to the new location X,Y."""
click.echo('Moving ship %s to %s,%s with speed %s' % (ship, x, y, speed)) click.echo("Moving ship {} to {},{} with speed {}".format(ship, x, y, speed))
@ship.command('shoot') @ship.command("shoot")
@click.argument('ship') @click.argument("ship")
@click.argument('x', type=float) @click.argument("x", type=float)
@click.argument('y', type=float) @click.argument("y", type=float)
def ship_shoot(ship, x, y): def ship_shoot(ship, x, y):
"""Makes SHIP fire to X,Y.""" """Makes SHIP fire to X,Y."""
click.echo('Ship %s fires to %s,%s' % (ship, x, y)) click.echo("Ship {} fires to {},{}".format(ship, x, y))
@cli.group('mine') @cli.group("mine")
def mine(): def mine():
"""Manages mines.""" """Manages mines."""
@mine.command('set') @mine.command("set")
@click.argument('x', type=float) @click.argument("x", type=float)
@click.argument('y', type=float) @click.argument("y", type=float)
@click.option('ty', '--moored', flag_value='moored', @click.option(
default=True, "ty",
help='Moored (anchored) mine. Default.') "--moored",
@click.option('ty', '--drifting', flag_value='drifting', flag_value="moored",
help='Drifting mine.') default=True,
help="Moored (anchored) mine. Default.",
)
@click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.")
def mine_set(x, y, ty): def mine_set(x, y, ty):
"""Sets a mine at a specific coordinate.""" """Sets a mine at a specific coordinate."""
click.echo('Set %s mine at %s,%s' % (ty, x, y)) click.echo("Set {} mine at {},{}".format(ty, x, y))
@mine.command('remove') @mine.command("remove")
@click.argument('x', type=float) @click.argument("x", type=float)
@click.argument('y', type=float) @click.argument("y", type=float)
def mine_remove(x, y): def mine_remove(x, y):
"""Removes a mine at a specific coordinate.""" """Removes a mine at a specific coordinate."""
click.echo('Removed mine at %s,%s' % (x, y)) click.echo("Removed mine at {},{}".format(x, y))

View file

@ -1,15 +1,13 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-naval', name="click-example-naval",
version='2.0', version="2.0",
py_modules=['naval'], py_modules=["naval"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
'click', entry_points="""
],
entry_points='''
[console_scripts] [console_scripts]
naval=naval:cli naval=naval:cli
''', """,
) )

View file

@ -1,12 +1,11 @@
import os import os
import sys
import posixpath import posixpath
import sys
import click import click
class Repo(object): class Repo(object):
def __init__(self, home): def __init__(self, home):
self.home = home self.home = home
self.config = {} self.config = {}
@ -15,23 +14,32 @@ class Repo(object):
def set_config(self, key, value): def set_config(self, key, value):
self.config[key] = value self.config[key] = value
if self.verbose: if self.verbose:
click.echo(' config[%s] = %s' % (key, value), file=sys.stderr) click.echo(" config[{}] = {}".format(key, value), file=sys.stderr)
def __repr__(self): def __repr__(self):
return '<Repo %r>' % self.home return "<Repo {}>".format(self.home)
pass_repo = click.make_pass_decorator(Repo) pass_repo = click.make_pass_decorator(Repo)
@click.group() @click.group()
@click.option('--repo-home', envvar='REPO_HOME', default='.repo', @click.option(
metavar='PATH', help='Changes the repository folder location.') "--repo-home",
@click.option('--config', nargs=2, multiple=True, envvar="REPO_HOME",
metavar='KEY VALUE', help='Overrides a config key/value pair.') default=".repo",
@click.option('--verbose', '-v', is_flag=True, metavar="PATH",
help='Enables verbose mode.') help="Changes the repository folder location.",
@click.version_option('1.0') )
@click.option(
"--config",
nargs=2,
multiple=True,
metavar="KEY VALUE",
help="Overrides a config key/value pair.",
)
@click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode.")
@click.version_option("1.0")
@click.pass_context @click.pass_context
def cli(ctx, repo_home, config, verbose): def cli(ctx, repo_home, config, verbose):
"""Repo is a command line tool that showcases how to build complex """Repo is a command line tool that showcases how to build complex
@ -50,12 +58,16 @@ def cli(ctx, repo_home, config, verbose):
@cli.command() @cli.command()
@click.argument('src') @click.argument("src")
@click.argument('dest', required=False) @click.argument("dest", required=False)
@click.option('--shallow/--deep', default=False, @click.option(
help='Makes a checkout shallow or deep. Deep by default.') "--shallow/--deep",
@click.option('--rev', '-r', default='HEAD', default=False,
help='Clone a specific revision instead of HEAD.') help="Makes a checkout shallow or deep. Deep by default.",
)
@click.option(
"--rev", "-r", default="HEAD", help="Clone a specific revision instead of HEAD."
)
@pass_repo @pass_repo
def clone(repo, src, dest, shallow, rev): def clone(repo, src, dest, shallow, rev):
"""Clones a repository. """Clones a repository.
@ -65,12 +77,12 @@ def clone(repo, src, dest, shallow, rev):
of SRC and create that folder. of SRC and create that folder.
""" """
if dest is None: if dest is None:
dest = posixpath.split(src)[-1] or '.' dest = posixpath.split(src)[-1] or "."
click.echo('Cloning repo %s to %s' % (src, os.path.abspath(dest))) click.echo("Cloning repo {} to {}".format(src, os.path.abspath(dest)))
repo.home = dest repo.home = dest
if shallow: if shallow:
click.echo('Making shallow checkout') click.echo("Making shallow checkout")
click.echo('Checking out revision %s' % rev) click.echo("Checking out revision {}".format(rev))
@cli.command() @cli.command()
@ -81,33 +93,35 @@ def delete(repo):
This will throw away the current repository. This will throw away the current repository.
""" """
click.echo('Destroying repo %s' % repo.home) click.echo("Destroying repo {}".format(repo.home))
click.echo('Deleted!') click.echo("Deleted!")
@cli.command() @cli.command()
@click.option('--username', prompt=True, @click.option("--username", prompt=True, help="The developer's shown username.")
help='The developer\'s shown username.') @click.option("--email", prompt="E-Mail", help="The developer's email address")
@click.option('--email', prompt='E-Mail', @click.password_option(help="The login password.")
help='The developer\'s email address')
@click.password_option(help='The login password.')
@pass_repo @pass_repo
def setuser(repo, username, email, password): def setuser(repo, username, email, password):
"""Sets the user credentials. """Sets the user credentials.
This will override the current user config. This will override the current user config.
""" """
repo.set_config('username', username) repo.set_config("username", username)
repo.set_config('email', email) repo.set_config("email", email)
repo.set_config('password', '*' * len(password)) repo.set_config("password", "*" * len(password))
click.echo('Changed credentials.') click.echo("Changed credentials.")
@cli.command() @cli.command()
@click.option('--message', '-m', multiple=True, @click.option(
help='The commit message. If provided multiple times each ' "--message",
'argument gets converted into a new line.') "-m",
@click.argument('files', nargs=-1, type=click.Path()) multiple=True,
help="The commit message. If provided multiple times each"
" argument gets converted into a new line.",
)
@click.argument("files", nargs=-1, type=click.Path())
@pass_repo @pass_repo
def commit(repo, files, message): def commit(repo, files, message):
"""Commits outstanding changes. """Commits outstanding changes.
@ -119,33 +133,34 @@ def commit(repo, files, message):
will be committed. will be committed.
""" """
if not message: if not message:
marker = '# Files to be committed:' marker = "# Files to be committed:"
hint = ['', '', marker, '#'] hint = ["", "", marker, "#"]
for file in files: for file in files:
hint.append('# U %s' % file) hint.append("# U {}".format(file))
message = click.edit('\n'.join(hint)) message = click.edit("\n".join(hint))
if message is None: if message is None:
click.echo('Aborted!') click.echo("Aborted!")
return return
msg = message.split(marker)[0].rstrip() msg = message.split(marker)[0].rstrip()
if not msg: if not msg:
click.echo('Aborted! Empty commit message') click.echo("Aborted! Empty commit message")
return return
else: else:
msg = '\n'.join(message) msg = "\n".join(message)
click.echo('Files to be committed: %s' % (files,)) click.echo("Files to be committed: {}".format(files))
click.echo('Commit message:\n' + msg) click.echo("Commit message:\n{}".format(msg))
@cli.command(short_help='Copies files.') @cli.command(short_help="Copies files.")
@click.option('--force', is_flag=True, @click.option(
help='forcibly copy over an existing managed file') "--force", is_flag=True, help="forcibly copy over an existing managed file"
@click.argument('src', nargs=-1, type=click.Path()) )
@click.argument('dst', type=click.Path()) @click.argument("src", nargs=-1, type=click.Path())
@click.argument("dst", type=click.Path())
@pass_repo @pass_repo
def copy(repo, src, dst, force): def copy(repo, src, dst, force):
"""Copies one or multiple files to a new location. This copies all """Copies one or multiple files to a new location. This copies all
files from SRC to DST. files from SRC to DST.
""" """
for fn in src: for fn in src:
click.echo('Copy from %s -> %s' % (fn, dst)) click.echo("Copy from {} -> {}".format(fn, dst))

View file

@ -1,15 +1,13 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-repo', name="click-example-repo",
version='0.1', version="0.1",
py_modules=['repo'], py_modules=["repo"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
'click', entry_points="""
],
entry_points='''
[console_scripts] [console_scripts]
repo=repo:cli repo=repo:cli
''', """,
) )

View file

@ -1,17 +1,17 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-termui', name="click-example-termui",
version='1.0', version="1.0",
py_modules=['termui'], py_modules=["termui"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'click', "click",
# Colorama is only required for Windows. # Colorama is only required for Windows.
'colorama', "colorama",
], ],
entry_points=''' entry_points="""
[console_scripts] [console_scripts]
termui=termui:cli termui=termui:cli
''', """,
) )

View file

@ -1,13 +1,9 @@
# coding: utf-8 # coding: utf-8
import click
import math import math
import time
import random import random
import time
try: import click
range_type = xrange
except NameError:
range_type = range
@click.group() @click.group()
@ -19,26 +15,30 @@ def cli():
@cli.command() @cli.command()
def colordemo(): def colordemo():
"""Demonstrates ANSI color support.""" """Demonstrates ANSI color support."""
for color in 'red', 'green', 'blue': for color in "red", "green", "blue":
click.echo(click.style('I am colored %s' % color, fg=color)) click.echo(click.style("I am colored {}".format(color), fg=color))
click.echo(click.style('I am background colored %s' % color, bg=color)) click.echo(click.style("I am background colored {}".format(color), bg=color))
@cli.command() @cli.command()
def pager(): def pager():
"""Demonstrates using the pager.""" """Demonstrates using the pager."""
lines = [] lines = []
for x in range_type(200): for x in range(200):
lines.append('%s. Hello World!' % click.style(str(x), fg='green')) lines.append("{}. Hello World!".format(click.style(str(x), fg="green")))
click.echo_via_pager('\n'.join(lines)) click.echo_via_pager("\n".join(lines))
@cli.command() @cli.command()
@click.option('--count', default=8000, type=click.IntRange(1, 100000), @click.option(
help='The number of items to process.') "--count",
default=8000,
type=click.IntRange(1, 100000),
help="The number of items to process.",
)
def progress(count): def progress(count):
"""Demonstrates the progress bar.""" """Demonstrates the progress bar."""
items = range_type(count) items = range(count)
def process_slowly(item): def process_slowly(item):
time.sleep(0.002 * random.random()) time.sleep(0.002 * random.random())
@ -48,54 +48,68 @@ def progress(count):
if random.random() > 0.3: if random.random() > 0.3:
yield item yield item
with click.progressbar(items, label='Processing accounts', with click.progressbar(
fill_char=click.style('#', fg='green')) as bar: items, label="Processing accounts", fill_char=click.style("#", fg="green")
) as bar:
for item in bar: for item in bar:
process_slowly(item) process_slowly(item)
def show_item(item): def show_item(item):
if item is not None: if item is not None:
return 'Item #%d' % item return "Item #{}".format(item)
with click.progressbar(filter(items), label='Committing transaction', with click.progressbar(
fill_char=click.style('#', fg='yellow'), filter(items),
item_show_func=show_item) as bar: label="Committing transaction",
fill_char=click.style("#", fg="yellow"),
item_show_func=show_item,
) as bar:
for item in bar: for item in bar:
process_slowly(item) process_slowly(item)
with click.progressbar(length=count, label='Counting', with click.progressbar(
bar_template='%(label)s %(bar)s | %(info)s', length=count,
fill_char=click.style(u'', fg='cyan'), label="Counting",
empty_char=' ') as bar: bar_template="%(label)s %(bar)s | %(info)s",
fill_char=click.style(u"", fg="cyan"),
empty_char=" ",
) as bar:
for item in bar: for item in bar:
process_slowly(item) process_slowly(item)
with click.progressbar(length=count, width=0, show_percent=False, with click.progressbar(
show_eta=False, length=count,
fill_char=click.style('#', fg='magenta')) as bar: width=0,
show_percent=False,
show_eta=False,
fill_char=click.style("#", fg="magenta"),
) as bar:
for item in bar: for item in bar:
process_slowly(item) process_slowly(item)
# 'Non-linear progress bar' # 'Non-linear progress bar'
steps = [math.exp( x * 1. / 20) - 1 for x in range(20)] steps = [math.exp(x * 1.0 / 20) - 1 for x in range(20)]
count = int(sum(steps)) count = int(sum(steps))
with click.progressbar(length=count, show_percent=False, with click.progressbar(
label='Slowing progress bar', length=count,
fill_char=click.style(u'', fg='green')) as bar: show_percent=False,
label="Slowing progress bar",
fill_char=click.style(u"", fg="green"),
) as bar:
for item in steps: for item in steps:
time.sleep(item) time.sleep(item)
bar.update(item) bar.update(item)
@cli.command() @cli.command()
@click.argument('url') @click.argument("url")
def open(url): def open(url):
"""Opens a file or URL In the default application.""" """Opens a file or URL In the default application."""
click.launch(url) click.launch(url)
@cli.command() @cli.command()
@click.argument('url') @click.argument("url")
def locate(url): def locate(url):
"""Opens a file or URL In the default application.""" """Opens a file or URL In the default application."""
click.launch(url, locate=True) click.launch(url, locate=True)
@ -104,16 +118,16 @@ def locate(url):
@cli.command() @cli.command()
def edit(): def edit():
"""Opens an editor with some text in it.""" """Opens an editor with some text in it."""
MARKER = '# Everything below is ignored\n' MARKER = "# Everything below is ignored\n"
message = click.edit('\n\n' + MARKER) message = click.edit("\n\n{}".format(MARKER))
if message is not None: if message is not None:
msg = message.split(MARKER, 1)[0].rstrip('\n') msg = message.split(MARKER, 1)[0].rstrip("\n")
if not msg: if not msg:
click.echo('Empty message!') click.echo("Empty message!")
else: else:
click.echo('Message:\n' + msg) click.echo("Message:\n{}".format(msg))
else: else:
click.echo('You did not enter anything!') click.echo("You did not enter anything!")
@cli.command() @cli.command()
@ -131,26 +145,26 @@ def pause():
@cli.command() @cli.command()
def menu(): def menu():
"""Shows a simple menu.""" """Shows a simple menu."""
menu = 'main' menu = "main"
while 1: while 1:
if menu == 'main': if menu == "main":
click.echo('Main menu:') click.echo("Main menu:")
click.echo(' d: debug menu') click.echo(" d: debug menu")
click.echo(' q: quit') click.echo(" q: quit")
char = click.getchar() char = click.getchar()
if char == 'd': if char == "d":
menu = 'debug' menu = "debug"
elif char == 'q': elif char == "q":
menu = 'quit' menu = "quit"
else: else:
click.echo('Invalid input') click.echo("Invalid input")
elif menu == 'debug': elif menu == "debug":
click.echo('Debug menu') click.echo("Debug menu")
click.echo(' b: back') click.echo(" b: back")
char = click.getchar() char = click.getchar()
if char == 'b': if char == "b":
menu = 'main' menu = "main"
else: else:
click.echo('Invalid input') click.echo("Invalid input")
elif menu == 'quit': elif menu == "quit":
return return

View file

@ -1,15 +1,13 @@
from setuptools import setup from setuptools import setup
setup( setup(
name='click-example-validation', name="click-example-validation",
version='1.0', version="1.0",
py_modules=['validation'], py_modules=["validation"],
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=["click"],
'click', entry_points="""
],
entry_points='''
[console_scripts] [console_scripts]
validation=validation:cli validation=validation:cli
''', """,
) )

View file

@ -1,4 +1,5 @@
import click import click
try: try:
from urllib import parse as urlparse from urllib import parse as urlparse
except ImportError: except ImportError:
@ -7,27 +8,32 @@ except ImportError:
def validate_count(ctx, param, value): def validate_count(ctx, param, value):
if value < 0 or value % 2 != 0: if value < 0 or value % 2 != 0:
raise click.BadParameter('Should be a positive, even integer.') raise click.BadParameter("Should be a positive, even integer.")
return value return value
class URL(click.ParamType): class URL(click.ParamType):
name = 'url' name = "url"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
if not isinstance(value, tuple): if not isinstance(value, tuple):
value = urlparse.urlparse(value) value = urlparse.urlparse(value)
if value.scheme not in ('http', 'https'): if value.scheme not in ("http", "https"):
self.fail('invalid URL scheme (%s). Only HTTP URLs are ' self.fail(
'allowed' % value.scheme, param, ctx) "invalid URL scheme ({}). Only HTTP URLs are"
" allowed".format(value.scheme),
param,
ctx,
)
return value return value
@click.command() @click.command()
@click.option('--count', default=2, callback=validate_count, @click.option(
help='A positive even number.') "--count", default=2, callback=validate_count, help="A positive even number."
@click.option('--foo', help='A mysterious parameter.') )
@click.option('--url', help='A URL', type=URL()) @click.option("--foo", help="A mysterious parameter.")
@click.option("--url", help="A URL", type=URL())
@click.version_option() @click.version_option()
def cli(count, foo, url): def cli(count, foo, url):
"""Validation. """Validation.
@ -36,9 +42,11 @@ def cli(count, foo, url):
through callbacks, through a custom type as well as by validating through callbacks, through a custom type as well as by validating
manually in the function. manually in the function.
""" """
if foo is not None and foo != 'wat': if foo is not None and foo != "wat":
raise click.BadParameter('If a value is provided it needs to be the ' raise click.BadParameter(
'value "wat".', param_hint=['--foo']) 'If a value is provided it needs to be the value "wat".',
click.echo('count: %s' % count) param_hint=["--foo"],
click.echo('foo: %s' % foo) )
click.echo('url: %s' % repr(url)) click.echo("count: {}".format(count))
click.echo("foo: {}".format(foo))
click.echo("url: {!r}".format(url))

View file

@ -6,18 +6,30 @@ universal = 1
[tool:pytest] [tool:pytest]
testpaths = tests testpaths = tests
filterwarnings =
error
[coverage:run] [coverage:run]
branch = True branch = True
source = source =
click src
tests tests
[coverage:paths] [coverage:paths]
source = source =
click click
.tox/*/lib/python*/site-packages/click */site-packages
.tox/pypy/site-packages/click
[flake8]
select = B, E, F, W, B9, ISC
ignore =
E203
E501
E722
W503
max-line-length = 80
per-file-ignores =
src/click/__init__.py: F401
[egg_info] [egg_info]
tag_build = tag_build =

View file

@ -1,15 +1,17 @@
import io import io
import re import re
from setuptools import find_packages
from setuptools import setup from setuptools import setup
with io.open("README.rst", "rt", encoding="utf8") as f: with io.open("README.rst", "rt", encoding="utf8") as f:
readme = f.read() readme = f.read()
with io.open("click/__init__.py", "rt", encoding="utf8") as f: with io.open("src/click/__init__.py", "rt", encoding="utf8") as f:
version = re.search(r"__version__ = \'(.*?)\'", f.read()).group(1) version = re.search(r'__version__ = "(.*?)"', f.read()).group(1)
setup( setup(
name="Click", name="click",
version=version, version=version,
url="https://palletsprojects.com/p/click/", url="https://palletsprojects.com/p/click/",
project_urls={ project_urls={
@ -17,16 +19,15 @@ setup(
"Code": "https://github.com/pallets/click", "Code": "https://github.com/pallets/click",
"Issue tracker": "https://github.com/pallets/click/issues", "Issue tracker": "https://github.com/pallets/click/issues",
}, },
license="BSD", license="BSD-3-Clause",
author="Armin Ronacher", maintainer="Pallets",
author_email="armin.ronacher@active-4.com",
maintainer="Pallets Team",
maintainer_email="contact@palletsprojects.com", maintainer_email="contact@palletsprojects.com",
description="Composable command line interface toolkit", description="Composable command line interface toolkit",
long_description=readme, long_description=readme,
packages=["click"], packages=find_packages("src"),
package_dir={"": "src"},
include_package_data=True, include_package_data=True,
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@ -34,11 +35,6 @@ setup(
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2", "Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
], ],
) )

View file

@ -1,13 +1,11 @@
Metadata-Version: 1.2 Metadata-Version: 1.2
Name: Click Name: click
Version: 7.0 Version: 7.1.2
Summary: Composable command line interface toolkit Summary: Composable command line interface toolkit
Home-page: https://palletsprojects.com/p/click/ Home-page: https://palletsprojects.com/p/click/
Author: Armin Ronacher Maintainer: Pallets
Author-email: armin.ronacher@active-4.com
Maintainer: Pallets Team
Maintainer-email: contact@palletsprojects.com Maintainer-email: contact@palletsprojects.com
License: BSD License: BSD-3-Clause
Project-URL: Documentation, https://click.palletsprojects.com/ Project-URL: Documentation, https://click.palletsprojects.com/
Project-URL: Code, https://github.com/pallets/click Project-URL: Code, https://github.com/pallets/click
Project-URL: Issue tracker, https://github.com/pallets/click/issues Project-URL: Issue tracker, https://github.com/pallets/click/issues
@ -37,9 +35,7 @@ Description: \$ click\_
.. code-block:: text .. code-block:: text
$ pip install click $ pip install -U click
Click supports Python 3.4 and newer, Python 2.7, and PyPy.
.. _pip: https://pip.pypa.io/en/stable/quickstart/ .. _pip: https://pip.pypa.io/en/stable/quickstart/
@ -47,26 +43,21 @@ Description: \$ click\_
A Simple Example A Simple Example
---------------- ----------------
What does it look like? Here is an example of a simple Click program:
.. code-block:: python .. code-block:: python
import click import click
@click.command() @click.command()
@click.option("--count", default=1, help="Number of greetings.") @click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name", @click.option("--name", prompt="Your name", help="The person to greet.")
help="The person to greet.")
def hello(count, name): def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times.""" """Simple program that greets NAME for a total of COUNT times."""
for _ in range(count): for _ in range(count):
click.echo("Hello, %s!" % name) click.echo(f"Hello, {name}!")
if __name__ == '__main__': if __name__ == '__main__':
hello() hello()
And what it looks like when run:
.. code-block:: text .. code-block:: text
$ python hello.py --count=3 $ python hello.py --count=3
@ -90,18 +81,13 @@ Description: \$ click\_
Links Links
----- -----
* Website: https://palletsprojects.com/p/click/ - Website: https://palletsprojects.com/p/click/
* Documentation: https://click.palletsprojects.com/ - Documentation: https://click.palletsprojects.com/
* License: `BSD <https://github.com/pallets/click/blob/master/LICENSE.rst>`_ - Releases: https://pypi.org/project/click/
* Releases: https://pypi.org/project/click/ - Code: https://github.com/pallets/click
* Code: https://github.com/pallets/click - Issue tracker: https://github.com/pallets/click/issues
* Issue tracker: https://github.com/pallets/click/issues - Test status: https://dev.azure.com/pallets/click/_build
* Test status: - Official chat: https://discord.gg/t6rrQZH
* Linux, Mac: https://travis-ci.org/pallets/click
* Windows: https://ci.appveyor.com/project/pallets/click
* Test coverage: https://codecov.io/gh/pallets/click
Platform: UNKNOWN Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable Classifier: Development Status :: 5 - Production/Stable
@ -110,10 +96,5 @@ Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.4 Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*

View file

@ -1,33 +1,11 @@
CHANGES.rst CHANGES.rst
CONTRIBUTING.rst
LICENSE.rst LICENSE.rst
MANIFEST.in MANIFEST.in
README.rst README.rst
setup.cfg setup.cfg
setup.py setup.py
tox.ini tox.ini
Click.egg-info/PKG-INFO
Click.egg-info/SOURCES.txt
Click.egg-info/dependency_links.txt
Click.egg-info/top_level.txt
artwork/logo.svg artwork/logo.svg
click/__init__.py
click/_bashcomplete.py
click/_compat.py
click/_termui_impl.py
click/_textwrap.py
click/_unicodefun.py
click/_winconsole.py
click/core.py
click/decorators.py
click/exceptions.py
click/formatting.py
click/globals.py
click/parser.py
click/termui.py
click/testing.py
click/types.py
click/utils.py
docs/Makefile docs/Makefile
docs/advanced.rst docs/advanced.rst
docs/api.rst docs/api.rst
@ -97,6 +75,27 @@ examples/termui/termui.py
examples/validation/README examples/validation/README
examples/validation/setup.py examples/validation/setup.py
examples/validation/validation.py examples/validation/validation.py
src/click/__init__.py
src/click/_bashcomplete.py
src/click/_compat.py
src/click/_termui_impl.py
src/click/_textwrap.py
src/click/_unicodefun.py
src/click/_winconsole.py
src/click/core.py
src/click/decorators.py
src/click/exceptions.py
src/click/formatting.py
src/click/globals.py
src/click/parser.py
src/click/termui.py
src/click/testing.py
src/click/types.py
src/click/utils.py
src/click.egg-info/PKG-INFO
src/click.egg-info/SOURCES.txt
src/click.egg-info/dependency_links.txt
src/click.egg-info/top_level.txt
tests/conftest.py tests/conftest.py
tests/test_arguments.py tests/test_arguments.py
tests/test_bashcomplete.py tests/test_bashcomplete.py

79
src/click/__init__.py Normal file
View file

@ -0,0 +1,79 @@
"""
Click is a simple Python module inspired by the stdlib optparse to make
writing command line scripts fun. Unlike other modules, it's based
around a simple API that does not come with too much magic and is
composable.
"""
from .core import Argument
from .core import BaseCommand
from .core import Command
from .core import CommandCollection
from .core import Context
from .core import Group
from .core import MultiCommand
from .core import Option
from .core import Parameter
from .decorators import argument
from .decorators import command
from .decorators import confirmation_option
from .decorators import group
from .decorators import help_option
from .decorators import make_pass_decorator
from .decorators import option
from .decorators import pass_context
from .decorators import pass_obj
from .decorators import password_option
from .decorators import version_option
from .exceptions import Abort
from .exceptions import BadArgumentUsage
from .exceptions import BadOptionUsage
from .exceptions import BadParameter
from .exceptions import ClickException
from .exceptions import FileError
from .exceptions import MissingParameter
from .exceptions import NoSuchOption
from .exceptions import UsageError
from .formatting import HelpFormatter
from .formatting import wrap_text
from .globals import get_current_context
from .parser import OptionParser
from .termui import clear
from .termui import confirm
from .termui import echo_via_pager
from .termui import edit
from .termui import get_terminal_size
from .termui import getchar
from .termui import launch
from .termui import pause
from .termui import progressbar
from .termui import prompt
from .termui import secho
from .termui import style
from .termui import unstyle
from .types import BOOL
from .types import Choice
from .types import DateTime
from .types import File
from .types import FLOAT
from .types import FloatRange
from .types import INT
from .types import IntRange
from .types import ParamType
from .types import Path
from .types import STRING
from .types import Tuple
from .types import UNPROCESSED
from .types import UUID
from .utils import echo
from .utils import format_filename
from .utils import get_app_dir
from .utils import get_binary_stream
from .utils import get_os_args
from .utils import get_text_stream
from .utils import open_file
# Controls if click should emit the warning about the use of unicode
# literals.
disable_unicode_literals_warning = False
__version__ = "7.1.2"

View file

@ -2,20 +2,22 @@ import copy
import os import os
import re import re
from .utils import echo from .core import Argument
from .core import MultiCommand
from .core import Option
from .parser import split_arg_string from .parser import split_arg_string
from .core import MultiCommand, Option, Argument
from .types import Choice from .types import Choice
from .utils import echo
try: try:
from collections import abc from collections import abc
except ImportError: except ImportError:
import collections as abc import collections as abc
WORDBREAK = '=' WORDBREAK = "="
# Note, only BASH version 4.4 and later have the nosort option. # Note, only BASH version 4.4 and later have the nosort option.
COMPLETION_SCRIPT_BASH = ''' COMPLETION_SCRIPT_BASH = """
%(complete_func)s() { %(complete_func)s() {
local IFS=$'\n' local IFS=$'\n'
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
@ -28,7 +30,8 @@ COMPLETION_SCRIPT_BASH = '''
local COMPLETION_OPTIONS="" local COMPLETION_OPTIONS=""
local BASH_VERSION_ARR=(${BASH_VERSION//./ }) local BASH_VERSION_ARR=(${BASH_VERSION//./ })
# Only BASH version 4.4 and later have the nosort option. # Only BASH version 4.4 and later have the nosort option.
if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \
&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
COMPLETION_OPTIONS="-o nosort" COMPLETION_OPTIONS="-o nosort"
fi fi
@ -36,13 +39,17 @@ COMPLETION_SCRIPT_BASH = '''
} }
%(complete_func)setup %(complete_func)setup
''' """
COMPLETION_SCRIPT_ZSH = """
#compdef %(script_names)s
COMPLETION_SCRIPT_ZSH = '''
%(complete_func)s() { %(complete_func)s() {
local -a completions local -a completions
local -a completions_with_descriptions local -a completions_with_descriptions
local -a response local -a response
(( ! $+commands[%(script_names)s] )) && return 1
response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\ response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\
COMP_CWORD=$((CURRENT-1)) \\ COMP_CWORD=$((CURRENT-1)) \\
%(autocomplete_var)s=\"complete_zsh\" \\ %(autocomplete_var)s=\"complete_zsh\" \\
@ -57,34 +64,51 @@ COMPLETION_SCRIPT_ZSH = '''
done done
if [ -n "$completions_with_descriptions" ]; then if [ -n "$completions_with_descriptions" ]; then
_describe -V unsorted completions_with_descriptions -U -Q _describe -V unsorted completions_with_descriptions -U
fi fi
if [ -n "$completions" ]; then if [ -n "$completions" ]; then
compadd -U -V unsorted -Q -a completions compadd -U -V unsorted -a completions
fi fi
compstate[insert]="automenu" compstate[insert]="automenu"
} }
compdef %(complete_func)s %(script_names)s compdef %(complete_func)s %(script_names)s
''' """
_invalid_ident_char_re = re.compile(r'[^a-zA-Z0-9_]') COMPLETION_SCRIPT_FISH = (
"complete --no-files --command %(script_names)s --arguments"
' "(env %(autocomplete_var)s=complete_fish'
" COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)"
' %(script_names)s)"'
)
_completion_scripts = {
"bash": COMPLETION_SCRIPT_BASH,
"zsh": COMPLETION_SCRIPT_ZSH,
"fish": COMPLETION_SCRIPT_FISH,
}
_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]")
def get_completion_script(prog_name, complete_var, shell): def get_completion_script(prog_name, complete_var, shell):
cf_name = _invalid_ident_char_re.sub('', prog_name.replace('-', '_')) cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_"))
script = COMPLETION_SCRIPT_ZSH if shell == 'zsh' else COMPLETION_SCRIPT_BASH script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH)
return (script % { return (
'complete_func': '_%s_completion' % cf_name, script
'script_names': prog_name, % {
'autocomplete_var': complete_var, "complete_func": "_{}_completion".format(cf_name),
}).strip() + ';' "script_names": prog_name,
"autocomplete_var": complete_var,
}
).strip() + ";"
def resolve_ctx(cli, prog_name, args): def resolve_ctx(cli, prog_name, args):
""" """Parse into a hierarchy of contexts. Contexts are connected
Parse into a hierarchy of contexts. Contexts are connected through the parent variable. through the parent variable.
:param cli: command definition :param cli: command definition
:param prog_name: the program that is running :param prog_name: the program that is running
:param args: full list of args :param args: full list of args
@ -98,8 +122,9 @@ def resolve_ctx(cli, prog_name, args):
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
if cmd is None: if cmd is None:
return ctx return ctx
ctx = cmd.make_context(cmd_name, args, parent=ctx, ctx = cmd.make_context(
resilient_parsing=True) cmd_name, args, parent=ctx, resilient_parsing=True
)
args = ctx.protected_args + ctx.args args = ctx.protected_args + ctx.args
else: else:
# Walk chained subcommand contexts saving the last one. # Walk chained subcommand contexts saving the last one.
@ -107,10 +132,14 @@ def resolve_ctx(cli, prog_name, args):
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) cmd_name, cmd, args = ctx.command.resolve_command(ctx, args)
if cmd is None: if cmd is None:
return ctx return ctx
sub_ctx = cmd.make_context(cmd_name, args, parent=ctx, sub_ctx = cmd.make_context(
allow_extra_args=True, cmd_name,
allow_interspersed_args=False, args,
resilient_parsing=True) parent=ctx,
allow_extra_args=True,
allow_interspersed_args=False,
resilient_parsing=True,
)
args = sub_ctx.args args = sub_ctx.args
ctx = sub_ctx ctx = sub_ctx
args = sub_ctx.protected_args + sub_ctx.args args = sub_ctx.protected_args + sub_ctx.args
@ -122,25 +151,29 @@ def resolve_ctx(cli, prog_name, args):
def start_of_option(param_str): def start_of_option(param_str):
""" """
:param param_str: param_str to check :param param_str: param_str to check
:return: whether or not this is the start of an option declaration (i.e. starts "-" or "--") :return: whether or not this is the start of an option declaration
(i.e. starts "-" or "--")
""" """
return param_str and param_str[:1] == '-' return param_str and param_str[:1] == "-"
def is_incomplete_option(all_args, cmd_param): def is_incomplete_option(all_args, cmd_param):
""" """
:param all_args: the full original list of args supplied :param all_args: the full original list of args supplied
:param cmd_param: the current command paramter :param cmd_param: the current command paramter
:return: whether or not the last option declaration (i.e. starts "-" or "--") is incomplete and :return: whether or not the last option declaration (i.e. starts
corresponds to this cmd_param. In other words whether this cmd_param option can still accept "-" or "--") is incomplete and corresponds to this cmd_param. In
values other words whether this cmd_param option can still accept
values
""" """
if not isinstance(cmd_param, Option): if not isinstance(cmd_param, Option):
return False return False
if cmd_param.is_flag: if cmd_param.is_flag:
return False return False
last_option = None last_option = None
for index, arg_str in enumerate(reversed([arg for arg in all_args if arg != WORDBREAK])): for index, arg_str in enumerate(
reversed([arg for arg in all_args if arg != WORDBREAK])
):
if index + 1 > cmd_param.nargs: if index + 1 > cmd_param.nargs:
break break
if start_of_option(arg_str): if start_of_option(arg_str):
@ -151,10 +184,12 @@ def is_incomplete_option(all_args, cmd_param):
def is_incomplete_argument(current_params, cmd_param): def is_incomplete_argument(current_params, cmd_param):
""" """
:param current_params: the current params and values for this argument as already entered :param current_params: the current params and values for this
argument as already entered
:param cmd_param: the current command parameter :param cmd_param: the current command parameter
:return: whether or not the last argument is incomplete and corresponds to this cmd_param. In :return: whether or not the last argument is incomplete and
other words whether or not the this cmd_param argument can still accept values corresponds to this cmd_param. In other words whether or not the
this cmd_param argument can still accept values
""" """
if not isinstance(cmd_param, Argument): if not isinstance(cmd_param, Argument):
return False return False
@ -163,8 +198,11 @@ def is_incomplete_argument(current_params, cmd_param):
return True return True
if cmd_param.nargs == -1: if cmd_param.nargs == -1:
return True return True
if isinstance(current_param_values, abc.Iterable) \ if (
and cmd_param.nargs > 1 and len(current_param_values) < cmd_param.nargs: isinstance(current_param_values, abc.Iterable)
and cmd_param.nargs > 1
and len(current_param_values) < cmd_param.nargs
):
return True return True
return False return False
@ -180,14 +218,16 @@ def get_user_autocompletions(ctx, args, incomplete, cmd_param):
results = [] results = []
if isinstance(cmd_param.type, Choice): if isinstance(cmd_param.type, Choice):
# Choices don't support descriptions. # Choices don't support descriptions.
results = [(c, None) results = [
for c in cmd_param.type.choices if str(c).startswith(incomplete)] (c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete)
]
elif cmd_param.autocompletion is not None: elif cmd_param.autocompletion is not None:
dynamic_completions = cmd_param.autocompletion(ctx=ctx, dynamic_completions = cmd_param.autocompletion(
args=args, ctx=ctx, args=args, incomplete=incomplete
incomplete=incomplete) )
results = [c if isinstance(c, tuple) else (c, None) results = [
for c in dynamic_completions] c if isinstance(c, tuple) else (c, None) for c in dynamic_completions
]
return results return results
@ -208,15 +248,25 @@ def add_subcommand_completions(ctx, incomplete, completions_out):
# Add subcommand completions. # Add subcommand completions.
if isinstance(ctx.command, MultiCommand): if isinstance(ctx.command, MultiCommand):
completions_out.extend( completions_out.extend(
[(c.name, c.get_short_help_str()) for c in get_visible_commands_starting_with(ctx, incomplete)]) [
(c.name, c.get_short_help_str())
for c in get_visible_commands_starting_with(ctx, incomplete)
]
)
# Walk up the context list and add any other completion possibilities from chained commands # Walk up the context list and add any other completion
# possibilities from chained commands
while ctx.parent is not None: while ctx.parent is not None:
ctx = ctx.parent ctx = ctx.parent
if isinstance(ctx.command, MultiCommand) and ctx.command.chain: if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
remaining_commands = [c for c in get_visible_commands_starting_with(ctx, incomplete) remaining_commands = [
if c.name not in ctx.protected_args] c
completions_out.extend([(c.name, c.get_short_help_str()) for c in remaining_commands]) for c in get_visible_commands_starting_with(ctx, incomplete)
if c.name not in ctx.protected_args
]
completions_out.extend(
[(c.name, c.get_short_help_str()) for c in remaining_commands]
)
def get_choices(cli, prog_name, args, incomplete): def get_choices(cli, prog_name, args, incomplete):
@ -233,23 +283,30 @@ def get_choices(cli, prog_name, args, incomplete):
if ctx is None: if ctx is None:
return [] return []
# In newer versions of bash long opts with '='s are partitioned, but it's easier to parse has_double_dash = "--" in all_args
# without the '='
# In newer versions of bash long opts with '='s are partitioned, but
# it's easier to parse without the '='
if start_of_option(incomplete) and WORDBREAK in incomplete: if start_of_option(incomplete) and WORDBREAK in incomplete:
partition_incomplete = incomplete.partition(WORDBREAK) partition_incomplete = incomplete.partition(WORDBREAK)
all_args.append(partition_incomplete[0]) all_args.append(partition_incomplete[0])
incomplete = partition_incomplete[2] incomplete = partition_incomplete[2]
elif incomplete == WORDBREAK: elif incomplete == WORDBREAK:
incomplete = '' incomplete = ""
completions = [] completions = []
if start_of_option(incomplete): if not has_double_dash and start_of_option(incomplete):
# completions for partial options # completions for partial options
for param in ctx.command.params: for param in ctx.command.params:
if isinstance(param, Option) and not param.hidden: if isinstance(param, Option) and not param.hidden:
param_opts = [param_opt for param_opt in param.opts + param_opts = [
param.secondary_opts if param_opt not in all_args or param.multiple] param_opt
completions.extend([(o, param.help) for o in param_opts if o.startswith(incomplete)]) for param_opt in param.opts + param.secondary_opts
if param_opt not in all_args or param.multiple
]
completions.extend(
[(o, param.help) for o in param_opts if o.startswith(incomplete)]
)
return completions return completions
# completion for option values from user supplied values # completion for option values from user supplied values
for param in ctx.command.params: for param in ctx.command.params:
@ -266,28 +323,53 @@ def get_choices(cli, prog_name, args, incomplete):
def do_complete(cli, prog_name, include_descriptions): def do_complete(cli, prog_name, include_descriptions):
cwords = split_arg_string(os.environ['COMP_WORDS']) cwords = split_arg_string(os.environ["COMP_WORDS"])
cword = int(os.environ['COMP_CWORD']) cword = int(os.environ["COMP_CWORD"])
args = cwords[1:cword] args = cwords[1:cword]
try: try:
incomplete = cwords[cword] incomplete = cwords[cword]
except IndexError: except IndexError:
incomplete = '' incomplete = ""
for item in get_choices(cli, prog_name, args, incomplete): for item in get_choices(cli, prog_name, args, incomplete):
echo(item[0]) echo(item[0])
if include_descriptions: if include_descriptions:
# ZSH has trouble dealing with empty array parameters when returned from commands, so use a well defined character '_' to indicate no description is present. # ZSH has trouble dealing with empty array parameters when
echo(item[1] if item[1] else '_') # returned from commands, use '_' to indicate no description
# is present.
echo(item[1] if item[1] else "_")
return True
def do_complete_fish(cli, prog_name):
cwords = split_arg_string(os.environ["COMP_WORDS"])
incomplete = os.environ["COMP_CWORD"]
args = cwords[1:]
for item in get_choices(cli, prog_name, args, incomplete):
if item[1]:
echo("{arg}\t{desc}".format(arg=item[0], desc=item[1]))
else:
echo(item[0])
return True return True
def bashcomplete(cli, prog_name, complete_var, complete_instr): def bashcomplete(cli, prog_name, complete_var, complete_instr):
if complete_instr.startswith('source'): if "_" in complete_instr:
shell = 'zsh' if complete_instr == 'source_zsh' else 'bash' command, shell = complete_instr.split("_", 1)
else:
command = complete_instr
shell = "bash"
if command == "source":
echo(get_completion_script(prog_name, complete_var, shell)) echo(get_completion_script(prog_name, complete_var, shell))
return True return True
elif complete_instr == 'complete' or complete_instr == 'complete_zsh': elif command == "complete":
return do_complete(cli, prog_name, complete_instr == 'complete_zsh') if shell == "fish":
return do_complete_fish(cli, prog_name)
elif shell in {"bash", "zsh"}:
return do_complete(cli, prog_name, shell == "zsh")
return False return False

View file

@ -1,67 +1,80 @@
import re # flake8: noqa
import codecs
import io import io
import os import os
import re
import sys import sys
import codecs
from weakref import WeakKeyDictionary from weakref import WeakKeyDictionary
PY2 = sys.version_info[0] == 2 PY2 = sys.version_info[0] == 2
CYGWIN = sys.platform.startswith('cygwin') CYGWIN = sys.platform.startswith("cygwin")
MSYS2 = sys.platform.startswith("win") and ("GCC" in sys.version)
# Determine local App Engine environment, per Google's own suggestion # Determine local App Engine environment, per Google's own suggestion
APP_ENGINE = ('APPENGINE_RUNTIME' in os.environ and APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ.get(
'Development/' in os.environ['SERVER_SOFTWARE']) "SERVER_SOFTWARE", ""
WIN = sys.platform.startswith('win') and not APP_ENGINE )
WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2
DEFAULT_COLUMNS = 80 DEFAULT_COLUMNS = 80
_ansi_re = re.compile(r'\033\[((?:\d|;)*)([a-zA-Z])') _ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]")
def get_filesystem_encoding(): def get_filesystem_encoding():
return sys.getfilesystemencoding() or sys.getdefaultencoding() return sys.getfilesystemencoding() or sys.getdefaultencoding()
def _make_text_stream(stream, encoding, errors, def _make_text_stream(
force_readable=False, force_writable=False): stream, encoding, errors, force_readable=False, force_writable=False
):
if encoding is None: if encoding is None:
encoding = get_best_encoding(stream) encoding = get_best_encoding(stream)
if errors is None: if errors is None:
errors = 'replace' errors = "replace"
return _NonClosingTextIOWrapper(stream, encoding, errors, return _NonClosingTextIOWrapper(
line_buffering=True, stream,
force_readable=force_readable, encoding,
force_writable=force_writable) errors,
line_buffering=True,
force_readable=force_readable,
force_writable=force_writable,
)
def is_ascii_encoding(encoding): def is_ascii_encoding(encoding):
"""Checks if a given encoding is ascii.""" """Checks if a given encoding is ascii."""
try: try:
return codecs.lookup(encoding).name == 'ascii' return codecs.lookup(encoding).name == "ascii"
except LookupError: except LookupError:
return False return False
def get_best_encoding(stream): def get_best_encoding(stream):
"""Returns the default stream encoding if not found.""" """Returns the default stream encoding if not found."""
rv = getattr(stream, 'encoding', None) or sys.getdefaultencoding() rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
if is_ascii_encoding(rv): if is_ascii_encoding(rv):
return 'utf-8' return "utf-8"
return rv return rv
class _NonClosingTextIOWrapper(io.TextIOWrapper): class _NonClosingTextIOWrapper(io.TextIOWrapper):
def __init__(
def __init__(self, stream, encoding, errors, self,
force_readable=False, force_writable=False, **extra): stream,
self._stream = stream = _FixupStream(stream, force_readable, encoding,
force_writable) errors,
force_readable=False,
force_writable=False,
**extra
):
self._stream = stream = _FixupStream(stream, force_readable, force_writable)
io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra)
# The io module is a place where the Python 3 text behavior # The io module is a place where the Python 3 text behavior
# was forced upon Python 2, so we need to unbreak # was forced upon Python 2, so we need to unbreak
# it to look like Python 2. # it to look like Python 2.
if PY2: if PY2:
def write(self, x): def write(self, x):
if isinstance(x, str) or is_bytes(x): if isinstance(x, str) or is_bytes(x):
try: try:
@ -105,7 +118,7 @@ class _FixupStream(object):
return getattr(self._stream, name) return getattr(self._stream, name)
def read1(self, size): def read1(self, size):
f = getattr(self._stream, 'read1', None) f = getattr(self._stream, "read1", None)
if f is not None: if f is not None:
return f(size) return f(size)
# We only dispatch to readline instead of read in Python 2 as we # We only dispatch to readline instead of read in Python 2 as we
@ -118,7 +131,7 @@ class _FixupStream(object):
def readable(self): def readable(self):
if self._force_readable: if self._force_readable:
return True return True
x = getattr(self._stream, 'readable', None) x = getattr(self._stream, "readable", None)
if x is not None: if x is not None:
return x() return x()
try: try:
@ -130,20 +143,20 @@ class _FixupStream(object):
def writable(self): def writable(self):
if self._force_writable: if self._force_writable:
return True return True
x = getattr(self._stream, 'writable', None) x = getattr(self._stream, "writable", None)
if x is not None: if x is not None:
return x() return x()
try: try:
self._stream.write('') self._stream.write("")
except Exception: except Exception:
try: try:
self._stream.write(b'') self._stream.write(b"")
except Exception: except Exception:
return False return False
return True return True
def seekable(self): def seekable(self):
x = getattr(self._stream, 'seekable', None) x = getattr(self._stream, "seekable", None)
if x is not None: if x is not None:
return x() return x()
try: try:
@ -155,7 +168,6 @@ class _FixupStream(object):
if PY2: if PY2:
text_type = unicode text_type = unicode
bytes = str
raw_input = raw_input raw_input = raw_input
string_types = (str, unicode) string_types = (str, unicode)
int_types = (int, long) int_types = (int, long)
@ -165,7 +177,7 @@ if PY2:
def is_bytes(x): def is_bytes(x):
return isinstance(x, (buffer, bytearray)) return isinstance(x, (buffer, bytearray))
_identifier_re = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$') _identifier_re = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
# For Windows, we need to force stdout/stdin/stderr to binary if it's # For Windows, we need to force stdout/stdin/stderr to binary if it's
# fetched for that. This obviously is not the most correct way to do # fetched for that. This obviously is not the most correct way to do
@ -193,6 +205,7 @@ if PY2:
except ImportError: except ImportError:
pass pass
else: else:
def set_binary_mode(f): def set_binary_mode(f):
try: try:
fileno = f.fileno() fileno = f.fileno()
@ -207,6 +220,7 @@ if PY2:
except ImportError: except ImportError:
pass pass
else: else:
def set_binary_mode(f): def set_binary_mode(f):
try: try:
fileno = f.fileno() fileno = f.fileno()
@ -224,42 +238,42 @@ if PY2:
return set_binary_mode(sys.stdin) return set_binary_mode(sys.stdin)
def get_binary_stdout(): def get_binary_stdout():
_wrap_std_stream('stdout') _wrap_std_stream("stdout")
return set_binary_mode(sys.stdout) return set_binary_mode(sys.stdout)
def get_binary_stderr(): def get_binary_stderr():
_wrap_std_stream('stderr') _wrap_std_stream("stderr")
return set_binary_mode(sys.stderr) return set_binary_mode(sys.stderr)
def get_text_stdin(encoding=None, errors=None): def get_text_stdin(encoding=None, errors=None):
rv = _get_windows_console_stream(sys.stdin, encoding, errors) rv = _get_windows_console_stream(sys.stdin, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _make_text_stream(sys.stdin, encoding, errors, return _make_text_stream(sys.stdin, encoding, errors, force_readable=True)
force_readable=True)
def get_text_stdout(encoding=None, errors=None): def get_text_stdout(encoding=None, errors=None):
_wrap_std_stream('stdout') _wrap_std_stream("stdout")
rv = _get_windows_console_stream(sys.stdout, encoding, errors) rv = _get_windows_console_stream(sys.stdout, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _make_text_stream(sys.stdout, encoding, errors, return _make_text_stream(sys.stdout, encoding, errors, force_writable=True)
force_writable=True)
def get_text_stderr(encoding=None, errors=None): def get_text_stderr(encoding=None, errors=None):
_wrap_std_stream('stderr') _wrap_std_stream("stderr")
rv = _get_windows_console_stream(sys.stderr, encoding, errors) rv = _get_windows_console_stream(sys.stderr, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _make_text_stream(sys.stderr, encoding, errors, return _make_text_stream(sys.stderr, encoding, errors, force_writable=True)
force_writable=True)
def filename_to_ui(value): def filename_to_ui(value):
if isinstance(value, bytes): if isinstance(value, bytes):
value = value.decode(get_filesystem_encoding(), 'replace') value = value.decode(get_filesystem_encoding(), "replace")
return value return value
else: else:
import io import io
text_type = str text_type = str
raw_input = input raw_input = input
string_types = (str,) string_types = (str,)
@ -281,10 +295,10 @@ else:
def _is_binary_writer(stream, default=False): def _is_binary_writer(stream, default=False):
try: try:
stream.write(b'') stream.write(b"")
except Exception: except Exception:
try: try:
stream.write('') stream.write("")
return False return False
except Exception: except Exception:
pass pass
@ -299,7 +313,7 @@ else:
if _is_binary_reader(stream, False): if _is_binary_reader(stream, False):
return stream return stream
buf = getattr(stream, 'buffer', None) buf = getattr(stream, "buffer", None)
# Same situation here; this time we assume that the buffer is # Same situation here; this time we assume that the buffer is
# actually binary in case it's closed. # actually binary in case it's closed.
@ -314,7 +328,7 @@ else:
if _is_binary_writer(stream, False): if _is_binary_writer(stream, False):
return stream return stream
buf = getattr(stream, 'buffer', None) buf = getattr(stream, "buffer", None)
# Same situation here; this time we assume that the buffer is # Same situation here; this time we assume that the buffer is
# actually binary in case it's closed. # actually binary in case it's closed.
@ -327,136 +341,142 @@ else:
# to ASCII. This appears to happen in certain unittest # to ASCII. This appears to happen in certain unittest
# environments. It's not quite clear what the correct behavior is # environments. It's not quite clear what the correct behavior is
# but this at least will force Click to recover somehow. # but this at least will force Click to recover somehow.
return is_ascii_encoding(getattr(stream, 'encoding', None) or 'ascii') return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii")
def _is_compat_stream_attr(stream, attr, value):
"""A stream attribute is compatible if it is equal to the
desired value or the desired value is unset and the attribute
has a value.
"""
stream_value = getattr(stream, attr, None)
return stream_value == value or (value is None and stream_value is not None)
def _is_compatible_text_stream(stream, encoding, errors): def _is_compatible_text_stream(stream, encoding, errors):
stream_encoding = getattr(stream, 'encoding', None) """Check if a stream's encoding and errors attributes are
stream_errors = getattr(stream, 'errors', None) compatible with the desired values.
"""
return _is_compat_stream_attr(
stream, "encoding", encoding
) and _is_compat_stream_attr(stream, "errors", errors)
# Perfect match. def _force_correct_text_stream(
if stream_encoding == encoding and stream_errors == errors: text_stream,
return True encoding,
errors,
# Otherwise, it's only a compatible stream if we did not ask for is_binary,
# an encoding. find_binary,
if encoding is None: force_readable=False,
return stream_encoding is not None force_writable=False,
):
return False if is_binary(text_stream, False):
binary_reader = text_stream
def _force_correct_text_reader(text_reader, encoding, errors,
force_readable=False):
if _is_binary_reader(text_reader, False):
binary_reader = text_reader
else: else:
# If there is no target encoding set, we need to verify that the # If the stream looks compatible, and won't default to a
# reader is not actually misconfigured. # misconfigured ascii encoding, return it as-is.
if encoding is None and not _stream_is_misconfigured(text_reader): if _is_compatible_text_stream(text_stream, encoding, errors) and not (
return text_reader encoding is None and _stream_is_misconfigured(text_stream)
):
return text_stream
if _is_compatible_text_stream(text_reader, encoding, errors): # Otherwise, get the underlying binary reader.
return text_reader binary_reader = find_binary(text_stream)
# If the reader has no encoding, we try to find the underlying # If that's not possible, silently use the original reader
# binary reader for it. If that fails because the environment is # and get mojibake instead of exceptions.
# misconfigured, we silently go with the same reader because this
# is too common to happen. In that case, mojibake is better than
# exceptions.
binary_reader = _find_binary_reader(text_reader)
if binary_reader is None: if binary_reader is None:
return text_reader return text_stream
# At this point, we default the errors to replace instead of strict # Default errors to replace instead of strict in order to get
# because nobody handles those errors anyways and at this point # something that works.
# we're so fundamentally fucked that nothing can repair it.
if errors is None: if errors is None:
errors = 'replace' errors = "replace"
return _make_text_stream(binary_reader, encoding, errors,
force_readable=force_readable)
def _force_correct_text_writer(text_writer, encoding, errors, # Wrap the binary stream in a text stream with the correct
force_writable=False): # encoding parameters.
if _is_binary_writer(text_writer, False): return _make_text_stream(
binary_writer = text_writer binary_reader,
else: encoding,
# If there is no target encoding set, we need to verify that the errors,
# writer is not actually misconfigured. force_readable=force_readable,
if encoding is None and not _stream_is_misconfigured(text_writer): force_writable=force_writable,
return text_writer )
if _is_compatible_text_stream(text_writer, encoding, errors): def _force_correct_text_reader(text_reader, encoding, errors, force_readable=False):
return text_writer return _force_correct_text_stream(
text_reader,
encoding,
errors,
_is_binary_reader,
_find_binary_reader,
force_readable=force_readable,
)
# If the writer has no encoding, we try to find the underlying def _force_correct_text_writer(text_writer, encoding, errors, force_writable=False):
# binary writer for it. If that fails because the environment is return _force_correct_text_stream(
# misconfigured, we silently go with the same writer because this text_writer,
# is too common to happen. In that case, mojibake is better than encoding,
# exceptions. errors,
binary_writer = _find_binary_writer(text_writer) _is_binary_writer,
if binary_writer is None: _find_binary_writer,
return text_writer force_writable=force_writable,
)
# At this point, we default the errors to replace instead of strict
# because nobody handles those errors anyways and at this point
# we're so fundamentally fucked that nothing can repair it.
if errors is None:
errors = 'replace'
return _make_text_stream(binary_writer, encoding, errors,
force_writable=force_writable)
def get_binary_stdin(): def get_binary_stdin():
reader = _find_binary_reader(sys.stdin) reader = _find_binary_reader(sys.stdin)
if reader is None: if reader is None:
raise RuntimeError('Was not able to determine binary ' raise RuntimeError("Was not able to determine binary stream for sys.stdin.")
'stream for sys.stdin.')
return reader return reader
def get_binary_stdout(): def get_binary_stdout():
writer = _find_binary_writer(sys.stdout) writer = _find_binary_writer(sys.stdout)
if writer is None: if writer is None:
raise RuntimeError('Was not able to determine binary ' raise RuntimeError(
'stream for sys.stdout.') "Was not able to determine binary stream for sys.stdout."
)
return writer return writer
def get_binary_stderr(): def get_binary_stderr():
writer = _find_binary_writer(sys.stderr) writer = _find_binary_writer(sys.stderr)
if writer is None: if writer is None:
raise RuntimeError('Was not able to determine binary ' raise RuntimeError(
'stream for sys.stderr.') "Was not able to determine binary stream for sys.stderr."
)
return writer return writer
def get_text_stdin(encoding=None, errors=None): def get_text_stdin(encoding=None, errors=None):
rv = _get_windows_console_stream(sys.stdin, encoding, errors) rv = _get_windows_console_stream(sys.stdin, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _force_correct_text_reader(sys.stdin, encoding, errors, return _force_correct_text_reader(
force_readable=True) sys.stdin, encoding, errors, force_readable=True
)
def get_text_stdout(encoding=None, errors=None): def get_text_stdout(encoding=None, errors=None):
rv = _get_windows_console_stream(sys.stdout, encoding, errors) rv = _get_windows_console_stream(sys.stdout, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _force_correct_text_writer(sys.stdout, encoding, errors, return _force_correct_text_writer(
force_writable=True) sys.stdout, encoding, errors, force_writable=True
)
def get_text_stderr(encoding=None, errors=None): def get_text_stderr(encoding=None, errors=None):
rv = _get_windows_console_stream(sys.stderr, encoding, errors) rv = _get_windows_console_stream(sys.stderr, encoding, errors)
if rv is not None: if rv is not None:
return rv return rv
return _force_correct_text_writer(sys.stderr, encoding, errors, return _force_correct_text_writer(
force_writable=True) sys.stderr, encoding, errors, force_writable=True
)
def filename_to_ui(value): def filename_to_ui(value):
if isinstance(value, bytes): if isinstance(value, bytes):
value = value.decode(get_filesystem_encoding(), 'replace') value = value.decode(get_filesystem_encoding(), "replace")
else: else:
value = value.encode('utf-8', 'surrogateescape') \ value = value.encode("utf-8", "surrogateescape").decode("utf-8", "replace")
.decode('utf-8', 'replace')
return value return value
def get_streerror(e, default=None): def get_streerror(e, default=None):
if hasattr(e, 'strerror'): if hasattr(e, "strerror"):
msg = e.strerror msg = e.strerror
else: else:
if default is not None: if default is not None:
@ -464,60 +484,107 @@ def get_streerror(e, default=None):
else: else:
msg = str(e) msg = str(e)
if isinstance(msg, bytes): if isinstance(msg, bytes):
msg = msg.decode('utf-8', 'replace') msg = msg.decode("utf-8", "replace")
return msg return msg
def open_stream(filename, mode='r', encoding=None, errors='strict', def _wrap_io_open(file, mode, encoding, errors):
atomic=False): """On Python 2, :func:`io.open` returns a text file wrapper that
requires passing ``unicode`` to ``write``. Need to open the file in
binary mode then wrap it in a subclass that can write ``str`` and
``unicode``.
Also handles not passing ``encoding`` and ``errors`` in binary mode.
"""
binary = "b" in mode
if binary:
kwargs = {}
else:
kwargs = {"encoding": encoding, "errors": errors}
if not PY2 or binary:
return io.open(file, mode, **kwargs)
f = io.open(file, "{}b".format(mode.replace("t", "")))
return _make_text_stream(f, **kwargs)
def open_stream(filename, mode="r", encoding=None, errors="strict", atomic=False):
binary = "b" in mode
# Standard streams first. These are simple because they don't need # Standard streams first. These are simple because they don't need
# special handling for the atomic flag. It's entirely ignored. # special handling for the atomic flag. It's entirely ignored.
if filename == '-': if filename == "-":
if any(m in mode for m in ['w', 'a', 'x']): if any(m in mode for m in ["w", "a", "x"]):
if 'b' in mode: if binary:
return get_binary_stdout(), False return get_binary_stdout(), False
return get_text_stdout(encoding=encoding, errors=errors), False return get_text_stdout(encoding=encoding, errors=errors), False
if 'b' in mode: if binary:
return get_binary_stdin(), False return get_binary_stdin(), False
return get_text_stdin(encoding=encoding, errors=errors), False return get_text_stdin(encoding=encoding, errors=errors), False
# Non-atomic writes directly go out through the regular open functions. # Non-atomic writes directly go out through the regular open functions.
if not atomic: if not atomic:
if encoding is None: return _wrap_io_open(filename, mode, encoding, errors), True
return open(filename, mode), True
return io.open(filename, mode, encoding=encoding, errors=errors), True
# Some usability stuff for atomic writes # Some usability stuff for atomic writes
if 'a' in mode: if "a" in mode:
raise ValueError( raise ValueError(
'Appending to an existing file is not supported, because that ' "Appending to an existing file is not supported, because that"
'would involve an expensive `copy`-operation to a temporary ' " would involve an expensive `copy`-operation to a temporary"
'file. Open the file in normal `w`-mode and copy explicitly ' " file. Open the file in normal `w`-mode and copy explicitly"
'if that\'s what you\'re after.' " if that's what you're after."
) )
if 'x' in mode: if "x" in mode:
raise ValueError('Use the `overwrite`-parameter instead.') raise ValueError("Use the `overwrite`-parameter instead.")
if 'w' not in mode: if "w" not in mode:
raise ValueError('Atomic writes only make sense with `w`-mode.') raise ValueError("Atomic writes only make sense with `w`-mode.")
# Atomic writes are more complicated. They work by opening a file # Atomic writes are more complicated. They work by opening a file
# as a proxy in the same folder and then using the fdopen # as a proxy in the same folder and then using the fdopen
# functionality to wrap it in a Python file. Then we wrap it in an # functionality to wrap it in a Python file. Then we wrap it in an
# atomic file that moves the file over on close. # atomic file that moves the file over on close.
import tempfile import errno
fd, tmp_filename = tempfile.mkstemp(dir=os.path.dirname(filename), import random
prefix='.__atomic-write')
if encoding is not None: try:
f = io.open(fd, mode, encoding=encoding, errors=errors) perm = os.stat(filename).st_mode
else: except OSError:
f = os.fdopen(fd, mode) perm = None
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
if binary:
flags |= getattr(os, "O_BINARY", 0)
while True:
tmp_filename = os.path.join(
os.path.dirname(filename),
".__atomic-write{:08x}".format(random.randrange(1 << 32)),
)
try:
fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm)
break
except OSError as e:
if e.errno == errno.EEXIST or (
os.name == "nt"
and e.errno == errno.EACCES
and os.path.isdir(e.filename)
and os.access(e.filename, os.W_OK)
):
continue
raise
if perm is not None:
os.chmod(tmp_filename, perm) # in case perm includes bits in umask
f = _wrap_io_open(fd, mode, encoding, errors)
return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True
# Used in a destructor call, needs extra protection from interpreter cleanup. # Used in a destructor call, needs extra protection from interpreter cleanup.
if hasattr(os, 'replace'): if hasattr(os, "replace"):
_replace = os.replace _replace = os.replace
_can_replace = True _can_replace = True
else: else:
@ -526,7 +593,6 @@ else:
class _AtomicFile(object): class _AtomicFile(object):
def __init__(self, f, tmp_filename, real_filename): def __init__(self, f, tmp_filename, real_filename):
self._f = f self._f = f
self._tmp_filename = tmp_filename self._tmp_filename = tmp_filename
@ -568,14 +634,26 @@ get_winterm_size = None
def strip_ansi(value): def strip_ansi(value):
return _ansi_re.sub('', value) return _ansi_re.sub("", value)
def _is_jupyter_kernel_output(stream):
if WIN:
# TODO: Couldn't test on Windows, should't try to support until
# someone tests the details wrt colorama.
return
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
stream = stream._stream
return stream.__class__.__module__.startswith("ipykernel.")
def should_strip_ansi(stream=None, color=None): def should_strip_ansi(stream=None, color=None):
if color is None: if color is None:
if stream is None: if stream is None:
stream = sys.stdin stream = sys.stdin
return not isatty(stream) return not isatty(stream) and not _is_jupyter_kernel_output(stream)
return not color return not color
@ -590,16 +668,18 @@ if WIN:
def _get_argv_encoding(): def _get_argv_encoding():
import locale import locale
return locale.getpreferredencoding() return locale.getpreferredencoding()
if PY2: if PY2:
def raw_input(prompt=''):
def raw_input(prompt=""):
sys.stderr.flush() sys.stderr.flush()
if prompt: if prompt:
stdout = _default_text_stdout() stdout = _default_text_stdout()
stdout.write(prompt) stdout.write(prompt)
stdin = _default_text_stdin() stdin = _default_text_stdin()
return stdin.readline().rstrip('\r\n') return stdin.readline().rstrip("\r\n")
try: try:
import colorama import colorama
@ -641,11 +721,15 @@ if WIN:
def get_winterm_size(): def get_winterm_size():
win = colorama.win32.GetConsoleScreenBufferInfo( win = colorama.win32.GetConsoleScreenBufferInfo(
colorama.win32.STDOUT).srWindow colorama.win32.STDOUT
).srWindow
return win.Right - win.Left, win.Bottom - win.Top return win.Right - win.Left, win.Bottom - win.Top
else: else:
def _get_argv_encoding(): def _get_argv_encoding():
return getattr(sys.stdin, 'encoding', None) or get_filesystem_encoding() return getattr(sys.stdin, "encoding", None) or get_filesystem_encoding()
_get_windows_console_stream = lambda *x: None _get_windows_console_stream = lambda *x: None
_wrap_std_stream = lambda *x: None _wrap_std_stream = lambda *x: None
@ -664,6 +748,7 @@ def isatty(stream):
def _make_cached_stream_func(src_func, wrapper_func): def _make_cached_stream_func(src_func, wrapper_func):
cache = WeakKeyDictionary() cache = WeakKeyDictionary()
def func(): def func():
stream = src_func() stream = src_func()
try: try:
@ -679,25 +764,23 @@ def _make_cached_stream_func(src_func, wrapper_func):
except Exception: except Exception:
pass pass
return rv return rv
return func return func
_default_text_stdin = _make_cached_stream_func( _default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin)
lambda: sys.stdin, get_text_stdin) _default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout)
_default_text_stdout = _make_cached_stream_func( _default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr)
lambda: sys.stdout, get_text_stdout)
_default_text_stderr = _make_cached_stream_func(
lambda: sys.stderr, get_text_stderr)
binary_streams = { binary_streams = {
'stdin': get_binary_stdin, "stdin": get_binary_stdin,
'stdout': get_binary_stdout, "stdout": get_binary_stdout,
'stderr': get_binary_stderr, "stderr": get_binary_stderr,
} }
text_streams = { text_streams = {
'stdin': get_text_stdin, "stdin": get_text_stdin,
'stdout': get_text_stdout, "stdout": get_text_stdout,
'stderr': get_text_stderr, "stderr": get_text_stderr,
} }

View file

@ -1,34 +1,34 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
click._termui_impl
~~~~~~~~~~~~~~~~~~
This module contains implementations for the termui module. To keep the This module contains implementations for the termui module. To keep the
import time of Click down, some infrequently used functionality is import time of Click down, some infrequently used functionality is
placed in this module and only imported as needed. placed in this module and only imported as needed.
:copyright: © 2014 by the Pallets team.
:license: BSD, see LICENSE.rst for more details.
""" """
import contextlib
import math
import os import os
import sys import sys
import time import time
import math
import contextlib from ._compat import _default_text_stdout
from ._compat import _default_text_stdout, range_type, PY2, isatty, \ from ._compat import CYGWIN
open_stream, strip_ansi, term_len, get_best_encoding, WIN, int_types, \ from ._compat import get_best_encoding
CYGWIN from ._compat import int_types
from .utils import echo from ._compat import isatty
from ._compat import open_stream
from ._compat import range_type
from ._compat import strip_ansi
from ._compat import term_len
from ._compat import WIN
from .exceptions import ClickException from .exceptions import ClickException
from .utils import echo
if os.name == "nt":
if os.name == 'nt': BEFORE_BAR = "\r"
BEFORE_BAR = '\r' AFTER_BAR = "\n"
AFTER_BAR = '\n'
else: else:
BEFORE_BAR = '\r\033[?25l' BEFORE_BAR = "\r\033[?25l"
AFTER_BAR = '\033[?25h\n' AFTER_BAR = "\033[?25h\n"
def _length_hint(obj): def _length_hint(obj):
@ -44,19 +44,29 @@ def _length_hint(obj):
hint = get_hint(obj) hint = get_hint(obj)
except TypeError: except TypeError:
return None return None
if hint is NotImplemented or \ if hint is NotImplemented or not isinstance(hint, int_types) or hint < 0:
not isinstance(hint, int_types) or \
hint < 0:
return None return None
return hint return hint
class ProgressBar(object): class ProgressBar(object):
def __init__(
def __init__(self, iterable, length=None, fill_char='#', empty_char=' ', self,
bar_template='%(bar)s', info_sep=' ', show_eta=True, iterable,
show_percent=None, show_pos=False, item_show_func=None, length=None,
label=None, file=None, color=None, width=30): fill_char="#",
empty_char=" ",
bar_template="%(bar)s",
info_sep=" ",
show_eta=True,
show_percent=None,
show_pos=False,
item_show_func=None,
label=None,
file=None,
color=None,
width=30,
):
self.fill_char = fill_char self.fill_char = fill_char
self.empty_char = empty_char self.empty_char = empty_char
self.bar_template = bar_template self.bar_template = bar_template
@ -65,7 +75,7 @@ class ProgressBar(object):
self.show_percent = show_percent self.show_percent = show_percent
self.show_pos = show_pos self.show_pos = show_pos
self.item_show_func = item_show_func self.item_show_func = item_show_func
self.label = label or '' self.label = label or ""
if file is None: if file is None:
file = _default_text_stdout() file = _default_text_stdout()
self.file = file self.file = file
@ -77,7 +87,7 @@ class ProgressBar(object):
length = _length_hint(iterable) length = _length_hint(iterable)
if iterable is None: if iterable is None:
if length is None: if length is None:
raise TypeError('iterable or length is required') raise TypeError("iterable or length is required")
iterable = range_type(length) iterable = range_type(length)
self.iter = iter(iterable) self.iter = iter(iterable)
self.length = length self.length = length
@ -104,10 +114,21 @@ class ProgressBar(object):
def __iter__(self): def __iter__(self):
if not self.entered: if not self.entered:
raise RuntimeError('You need to use progress bars in a with block.') raise RuntimeError("You need to use progress bars in a with block.")
self.render_progress() self.render_progress()
return self.generator() return self.generator()
def __next__(self):
# Iteration is defined in terms of a generator function,
# returned by iter(self); use that to define next(). This works
# because `self.iter` is an iterable consumed by that generator,
# so it is re-entry safe. Calling `next(self.generator())`
# twice works and does "what you want".
return next(iter(self))
# Python 2 compat
next = __next__
def is_fast(self): def is_fast(self):
return time.time() - self.start <= self.short_limit return time.time() - self.start <= self.short_limit
@ -145,20 +166,19 @@ class ProgressBar(object):
hours = t % 24 hours = t % 24
t //= 24 t //= 24
if t > 0: if t > 0:
days = t return "{}d {:02}:{:02}:{:02}".format(t, hours, minutes, seconds)
return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds)
else: else:
return '%02d:%02d:%02d' % (hours, minutes, seconds) return "{:02}:{:02}:{:02}".format(hours, minutes, seconds)
return '' return ""
def format_pos(self): def format_pos(self):
pos = str(self.pos) pos = str(self.pos)
if self.length_known: if self.length_known:
pos += '/%s' % self.length pos += "/{}".format(self.length)
return pos return pos
def format_pct(self): def format_pct(self):
return ('% 4d%%' % int(self.pct * 100))[1:] return "{: 4}%".format(int(self.pct * 100))[1:]
def format_bar(self): def format_bar(self):
if self.length_known: if self.length_known:
@ -170,9 +190,13 @@ class ProgressBar(object):
else: else:
bar = list(self.empty_char * (self.width or 1)) bar = list(self.empty_char * (self.width or 1))
if self.time_per_iteration != 0: if self.time_per_iteration != 0:
bar[int((math.cos(self.pos * self.time_per_iteration) bar[
/ 2.0 + 0.5) * self.width)] = self.fill_char int(
bar = ''.join(bar) (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5)
* self.width
)
] = self.fill_char
bar = "".join(bar)
return bar return bar
def format_progress_line(self): def format_progress_line(self):
@ -193,11 +217,14 @@ class ProgressBar(object):
if item_info is not None: if item_info is not None:
info_bits.append(item_info) info_bits.append(item_info)
return (self.bar_template % { return (
'label': self.label, self.bar_template
'bar': self.format_bar(), % {
'info': self.info_sep.join(info_bits) "label": self.label,
}).rstrip() "bar": self.format_bar(),
"info": self.info_sep.join(info_bits),
}
).rstrip()
def render_progress(self): def render_progress(self):
from .termui import get_terminal_size from .termui import get_terminal_size
@ -214,7 +241,7 @@ class ProgressBar(object):
new_width = max(0, get_terminal_size()[0] - clutter_length) new_width = max(0, get_terminal_size()[0] - clutter_length)
if new_width < old_width: if new_width < old_width:
buf.append(BEFORE_BAR) buf.append(BEFORE_BAR)
buf.append(' ' * self.max_width) buf.append(" " * self.max_width)
self.max_width = new_width self.max_width = new_width
self.width = new_width self.width = new_width
@ -229,8 +256,8 @@ class ProgressBar(object):
self.max_width = line_len self.max_width = line_len
buf.append(line) buf.append(line)
buf.append(' ' * (clear_width - line_len)) buf.append(" " * (clear_width - line_len))
line = ''.join(buf) line = "".join(buf)
# Render the line only if it changed. # Render the line only if it changed.
if line != self._last_line and not self.is_fast(): if line != self._last_line and not self.is_fast():
@ -270,13 +297,19 @@ class ProgressBar(object):
self.finished = True self.finished = True
def generator(self): def generator(self):
"""Return a generator which yields the items added to the bar
during construction, and updates the progress bar *after* the
yielded block returns.
""" """
Returns a generator which yields the items added to the bar during # WARNING: the iterator interface for `ProgressBar` relies on
construction, and updates the progress bar *after* the yielded block # this and only works because this is a simple generator which
returns. # doesn't create or manage additional state. If this function
""" # changes, the impact should be evaluated both against
# `iter(bar)` and `next(bar)`. `next()` in particular may call
# `self.generator()` repeatedly, and this must remain safe in
# order for that interface to work.
if not self.entered: if not self.entered:
raise RuntimeError('You need to use progress bars in a with block.') raise RuntimeError("You need to use progress bars in a with block.")
if self.is_hidden: if self.is_hidden:
for rv in self.iter: for rv in self.iter:
@ -295,24 +328,25 @@ def pager(generator, color=None):
stdout = _default_text_stdout() stdout = _default_text_stdout()
if not isatty(sys.stdin) or not isatty(stdout): if not isatty(sys.stdin) or not isatty(stdout):
return _nullpager(stdout, generator, color) return _nullpager(stdout, generator, color)
pager_cmd = (os.environ.get('PAGER', None) or '').strip() pager_cmd = (os.environ.get("PAGER", None) or "").strip()
if pager_cmd: if pager_cmd:
if WIN: if WIN:
return _tempfilepager(generator, pager_cmd, color) return _tempfilepager(generator, pager_cmd, color)
return _pipepager(generator, pager_cmd, color) return _pipepager(generator, pager_cmd, color)
if os.environ.get('TERM') in ('dumb', 'emacs'): if os.environ.get("TERM") in ("dumb", "emacs"):
return _nullpager(stdout, generator, color) return _nullpager(stdout, generator, color)
if WIN or sys.platform.startswith('os2'): if WIN or sys.platform.startswith("os2"):
return _tempfilepager(generator, 'more <', color) return _tempfilepager(generator, "more <", color)
if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0:
return _pipepager(generator, 'less', color) return _pipepager(generator, "less", color)
import tempfile import tempfile
fd, filename = tempfile.mkstemp() fd, filename = tempfile.mkstemp()
os.close(fd) os.close(fd)
try: try:
if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: if hasattr(os, "system") and os.system('more "{}"'.format(filename)) == 0:
return _pipepager(generator, 'more', color) return _pipepager(generator, "more", color)
return _nullpager(stdout, generator, color) return _nullpager(stdout, generator, color)
finally: finally:
os.unlink(filename) os.unlink(filename)
@ -323,28 +357,28 @@ def _pipepager(generator, cmd, color):
pager through this might support colors. pager through this might support colors.
""" """
import subprocess import subprocess
env = dict(os.environ) env = dict(os.environ)
# If we're piping to less we might support colors under the # If we're piping to less we might support colors under the
# condition that # condition that
cmd_detail = cmd.rsplit('/', 1)[-1].split() cmd_detail = cmd.rsplit("/", 1)[-1].split()
if color is None and cmd_detail[0] == 'less': if color is None and cmd_detail[0] == "less":
less_flags = os.environ.get('LESS', '') + ' '.join(cmd_detail[1:]) less_flags = "{}{}".format(os.environ.get("LESS", ""), " ".join(cmd_detail[1:]))
if not less_flags: if not less_flags:
env['LESS'] = '-R' env["LESS"] = "-R"
color = True color = True
elif 'r' in less_flags or 'R' in less_flags: elif "r" in less_flags or "R" in less_flags:
color = True color = True
c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env)
env=env)
encoding = get_best_encoding(c.stdin) encoding = get_best_encoding(c.stdin)
try: try:
for text in generator: for text in generator:
if not color: if not color:
text = strip_ansi(text) text = strip_ansi(text)
c.stdin.write(text.encode(encoding, 'replace')) c.stdin.write(text.encode(encoding, "replace"))
except (IOError, KeyboardInterrupt): except (IOError, KeyboardInterrupt):
pass pass
else: else:
@ -370,16 +404,17 @@ def _pipepager(generator, cmd, color):
def _tempfilepager(generator, cmd, color): def _tempfilepager(generator, cmd, color):
"""Page through text by invoking a program on a temporary file.""" """Page through text by invoking a program on a temporary file."""
import tempfile import tempfile
filename = tempfile.mktemp() filename = tempfile.mktemp()
# TODO: This never terminates if the passed generator never terminates. # TODO: This never terminates if the passed generator never terminates.
text = "".join(generator) text = "".join(generator)
if not color: if not color:
text = strip_ansi(text) text = strip_ansi(text)
encoding = get_best_encoding(sys.stdout) encoding = get_best_encoding(sys.stdout)
with open_stream(filename, 'wb')[0] as f: with open_stream(filename, "wb")[0] as f:
f.write(text.encode(encoding)) f.write(text.encode(encoding))
try: try:
os.system(cmd + ' "' + filename + '"') os.system('{} "{}"'.format(cmd, filename))
finally: finally:
os.unlink(filename) os.unlink(filename)
@ -393,9 +428,7 @@ def _nullpager(stream, generator, color):
class Editor(object): class Editor(object):
def __init__(self, editor=None, env=None, require_save=True, extension=".txt"):
def __init__(self, editor=None, env=None, require_save=True,
extension='.txt'):
self.editor = editor self.editor = editor
self.env = env self.env = env
self.require_save = require_save self.require_save = require_save
@ -404,19 +437,20 @@ class Editor(object):
def get_editor(self): def get_editor(self):
if self.editor is not None: if self.editor is not None:
return self.editor return self.editor
for key in 'VISUAL', 'EDITOR': for key in "VISUAL", "EDITOR":
rv = os.environ.get(key) rv = os.environ.get(key)
if rv: if rv:
return rv return rv
if WIN: if WIN:
return 'notepad' return "notepad"
for editor in 'vim', 'nano': for editor in "sensible-editor", "vim", "nano":
if os.system('which %s >/dev/null 2>&1' % editor) == 0: if os.system("which {} >/dev/null 2>&1".format(editor)) == 0:
return editor return editor
return 'vi' return "vi"
def edit_file(self, filename): def edit_file(self, filename):
import subprocess import subprocess
editor = self.get_editor() editor = self.get_editor()
if self.env: if self.env:
environ = os.environ.copy() environ = os.environ.copy()
@ -424,47 +458,47 @@ class Editor(object):
else: else:
environ = None environ = None
try: try:
c = subprocess.Popen('%s "%s"' % (editor, filename), c = subprocess.Popen(
env=environ, shell=True) '{} "{}"'.format(editor, filename), env=environ, shell=True,
)
exit_code = c.wait() exit_code = c.wait()
if exit_code != 0: if exit_code != 0:
raise ClickException('%s: Editing failed!' % editor) raise ClickException("{}: Editing failed!".format(editor))
except OSError as e: except OSError as e:
raise ClickException('%s: Editing failed: %s' % (editor, e)) raise ClickException("{}: Editing failed: {}".format(editor, e))
def edit(self, text): def edit(self, text):
import tempfile import tempfile
text = text or '' text = text or ""
if text and not text.endswith('\n'): if text and not text.endswith("\n"):
text += '\n' text += "\n"
fd, name = tempfile.mkstemp(prefix='editor-', suffix=self.extension) fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension)
try: try:
if WIN: if WIN:
encoding = 'utf-8-sig' encoding = "utf-8-sig"
text = text.replace('\n', '\r\n') text = text.replace("\n", "\r\n")
else: else:
encoding = 'utf-8' encoding = "utf-8"
text = text.encode(encoding) text = text.encode(encoding)
f = os.fdopen(fd, 'wb') f = os.fdopen(fd, "wb")
f.write(text) f.write(text)
f.close() f.close()
timestamp = os.path.getmtime(name) timestamp = os.path.getmtime(name)
self.edit_file(name) self.edit_file(name)
if self.require_save \ if self.require_save and os.path.getmtime(name) == timestamp:
and os.path.getmtime(name) == timestamp:
return None return None
f = open(name, 'rb') f = open(name, "rb")
try: try:
rv = f.read() rv = f.read()
finally: finally:
f.close() f.close()
return rv.decode('utf-8-sig').replace('\r\n', '\n') return rv.decode("utf-8-sig").replace("\r\n", "\n")
finally: finally:
os.unlink(name) os.unlink(name)
@ -477,18 +511,18 @@ def open_url(url, wait=False, locate=False):
import urllib import urllib
except ImportError: except ImportError:
import urllib import urllib
if url.startswith('file://'): if url.startswith("file://"):
url = urllib.unquote(url[7:]) url = urllib.unquote(url[7:])
return url return url
if sys.platform == 'darwin': if sys.platform == "darwin":
args = ['open'] args = ["open"]
if wait: if wait:
args.append('-W') args.append("-W")
if locate: if locate:
args.append('-R') args.append("-R")
args.append(_unquote_file(url)) args.append(_unquote_file(url))
null = open('/dev/null', 'w') null = open("/dev/null", "w")
try: try:
return subprocess.Popen(args, stderr=null).wait() return subprocess.Popen(args, stderr=null).wait()
finally: finally:
@ -496,44 +530,44 @@ def open_url(url, wait=False, locate=False):
elif WIN: elif WIN:
if locate: if locate:
url = _unquote_file(url) url = _unquote_file(url)
args = 'explorer /select,"%s"' % _unquote_file( args = 'explorer /select,"{}"'.format(_unquote_file(url.replace('"', "")))
url.replace('"', ''))
else: else:
args = 'start %s "" "%s"' % ( args = 'start {} "" "{}"'.format(
wait and '/WAIT' or '', url.replace('"', '')) "/WAIT" if wait else "", url.replace('"', "")
)
return os.system(args) return os.system(args)
elif CYGWIN: elif CYGWIN:
if locate: if locate:
url = _unquote_file(url) url = _unquote_file(url)
args = 'cygstart "%s"' % (os.path.dirname(url).replace('"', '')) args = 'cygstart "{}"'.format(os.path.dirname(url).replace('"', ""))
else: else:
args = 'cygstart %s "%s"' % ( args = 'cygstart {} "{}"'.format("-w" if wait else "", url.replace('"', ""))
wait and '-w' or '', url.replace('"', ''))
return os.system(args) return os.system(args)
try: try:
if locate: if locate:
url = os.path.dirname(_unquote_file(url)) or '.' url = os.path.dirname(_unquote_file(url)) or "."
else: else:
url = _unquote_file(url) url = _unquote_file(url)
c = subprocess.Popen(['xdg-open', url]) c = subprocess.Popen(["xdg-open", url])
if wait: if wait:
return c.wait() return c.wait()
return 0 return 0
except OSError: except OSError:
if url.startswith(('http://', 'https://')) and not locate and not wait: if url.startswith(("http://", "https://")) and not locate and not wait:
import webbrowser import webbrowser
webbrowser.open(url) webbrowser.open(url)
return 0 return 0
return 1 return 1
def _translate_ch_to_exc(ch): def _translate_ch_to_exc(ch):
if ch == u'\x03': if ch == u"\x03":
raise KeyboardInterrupt() raise KeyboardInterrupt()
if ch == u'\x04' and not WIN: # Unix-like, Ctrl+D if ch == u"\x04" and not WIN: # Unix-like, Ctrl+D
raise EOFError() raise EOFError()
if ch == u'\x1a' and WIN: # Windows, Ctrl+Z if ch == u"\x1a" and WIN: # Windows, Ctrl+Z
raise EOFError() raise EOFError()
@ -580,12 +614,14 @@ if WIN:
func = msvcrt.getwch func = msvcrt.getwch
rv = func() rv = func()
if rv in (u'\x00', u'\xe0'): if rv in (u"\x00", u"\xe0"):
# \x00 and \xe0 are control characters that indicate special key, # \x00 and \xe0 are control characters that indicate special key,
# see above. # see above.
rv += func() rv += func()
_translate_ch_to_exc(rv) _translate_ch_to_exc(rv)
return rv return rv
else: else:
import tty import tty
import termios import termios
@ -593,7 +629,7 @@ else:
@contextlib.contextmanager @contextlib.contextmanager
def raw_terminal(): def raw_terminal():
if not isatty(sys.stdin): if not isatty(sys.stdin):
f = open('/dev/tty') f = open("/dev/tty")
fd = f.fileno() fd = f.fileno()
else: else:
fd = sys.stdin.fileno() fd = sys.stdin.fileno()
@ -614,7 +650,7 @@ else:
def getchar(echo): def getchar(echo):
with raw_terminal() as fd: with raw_terminal() as fd:
ch = os.read(fd, 32) ch = os.read(fd, 32)
ch = ch.decode(get_best_encoding(sys.stdin), 'replace') ch = ch.decode(get_best_encoding(sys.stdin), "replace")
if echo and isatty(sys.stdout): if echo and isatty(sys.stdout):
sys.stdout.write(ch) sys.stdout.write(ch)
_translate_ch_to_exc(ch) _translate_ch_to_exc(ch)

View file

@ -3,7 +3,6 @@ from contextlib import contextmanager
class TextWrapper(textwrap.TextWrapper): class TextWrapper(textwrap.TextWrapper):
def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
space_left = max(width - cur_len, 1) space_left = max(width - cur_len, 1)
@ -35,4 +34,4 @@ class TextWrapper(textwrap.TextWrapper):
if idx > 0: if idx > 0:
indent = self.subsequent_indent indent = self.subsequent_indent
rv.append(indent + line) rv.append(indent + line)
return '\n'.join(rv) return "\n".join(rv)

131
src/click/_unicodefun.py Normal file
View file

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

View file

@ -7,24 +7,42 @@
# compared to the original patches as we do not need to patch # compared to the original patches as we do not need to patch
# the entire interpreter but just work in our little world of # the entire interpreter but just work in our little world of
# echo and prmopt. # echo and prmopt.
import ctypes
import io import io
import os import os
import sys import sys
import zlib
import time import time
import ctypes import zlib
from ctypes import byref
from ctypes import c_char
from ctypes import c_char_p
from ctypes import c_int
from ctypes import c_ssize_t
from ctypes import c_ulong
from ctypes import c_void_p
from ctypes import POINTER
from ctypes import py_object
from ctypes import windll
from ctypes import WinError
from ctypes import WINFUNCTYPE
from ctypes.wintypes import DWORD
from ctypes.wintypes import HANDLE
from ctypes.wintypes import LPCWSTR
from ctypes.wintypes import LPWSTR
import msvcrt import msvcrt
from ._compat import _NonClosingTextIOWrapper, text_type, PY2
from ctypes import byref, POINTER, c_int, c_char, c_char_p, \ from ._compat import _NonClosingTextIOWrapper
c_void_p, py_object, c_ssize_t, c_ulong, windll, WINFUNCTYPE from ._compat import PY2
from ._compat import text_type
try: try:
from ctypes import pythonapi from ctypes import pythonapi
PyObject_GetBuffer = pythonapi.PyObject_GetBuffer PyObject_GetBuffer = pythonapi.PyObject_GetBuffer
PyBuffer_Release = pythonapi.PyBuffer_Release PyBuffer_Release = pythonapi.PyBuffer_Release
except ImportError: except ImportError:
pythonapi = None pythonapi = None
from ctypes.wintypes import LPWSTR, LPCWSTR
c_ssize_p = POINTER(c_ssize_t) c_ssize_p = POINTER(c_ssize_t)
@ -33,12 +51,15 @@ kernel32 = windll.kernel32
GetStdHandle = kernel32.GetStdHandle GetStdHandle = kernel32.GetStdHandle
ReadConsoleW = kernel32.ReadConsoleW ReadConsoleW = kernel32.ReadConsoleW
WriteConsoleW = kernel32.WriteConsoleW WriteConsoleW = kernel32.WriteConsoleW
GetConsoleMode = kernel32.GetConsoleMode
GetLastError = kernel32.GetLastError GetLastError = kernel32.GetLastError
GetCommandLineW = WINFUNCTYPE(LPWSTR)( GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
('GetCommandLineW', windll.kernel32)) CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(
CommandLineToArgvW = WINFUNCTYPE( ("CommandLineToArgvW", windll.shell32)
POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( )
('CommandLineToArgvW', windll.shell32)) LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)(
("LocalFree", windll.kernel32)
)
STDIN_HANDLE = GetStdHandle(-10) STDIN_HANDLE = GetStdHandle(-10)
@ -57,27 +78,27 @@ STDIN_FILENO = 0
STDOUT_FILENO = 1 STDOUT_FILENO = 1
STDERR_FILENO = 2 STDERR_FILENO = 2
EOF = b'\x1a' EOF = b"\x1a"
MAX_BYTES_WRITTEN = 32767 MAX_BYTES_WRITTEN = 32767
class Py_buffer(ctypes.Structure): class Py_buffer(ctypes.Structure):
_fields_ = [ _fields_ = [
('buf', c_void_p), ("buf", c_void_p),
('obj', py_object), ("obj", py_object),
('len', c_ssize_t), ("len", c_ssize_t),
('itemsize', c_ssize_t), ("itemsize", c_ssize_t),
('readonly', c_int), ("readonly", c_int),
('ndim', c_int), ("ndim", c_int),
('format', c_char_p), ("format", c_char_p),
('shape', c_ssize_p), ("shape", c_ssize_p),
('strides', c_ssize_p), ("strides", c_ssize_p),
('suboffsets', c_ssize_p), ("suboffsets", c_ssize_p),
('internal', c_void_p) ("internal", c_void_p),
] ]
if PY2: if PY2:
_fields_.insert(-1, ('smalltable', c_ssize_t * 2)) _fields_.insert(-1, ("smalltable", c_ssize_t * 2))
# On PyPy we cannot get buffers so our ability to operate here is # On PyPy we cannot get buffers so our ability to operate here is
@ -85,6 +106,7 @@ class Py_buffer(ctypes.Structure):
if pythonapi is None: if pythonapi is None:
get_buffer = None get_buffer = None
else: else:
def get_buffer(obj, writable=False): def get_buffer(obj, writable=False):
buf = Py_buffer() buf = Py_buffer()
flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE
@ -97,7 +119,6 @@ else:
class _WindowsConsoleRawIOBase(io.RawIOBase): class _WindowsConsoleRawIOBase(io.RawIOBase):
def __init__(self, handle): def __init__(self, handle):
self.handle = handle self.handle = handle
@ -107,7 +128,6 @@ class _WindowsConsoleRawIOBase(io.RawIOBase):
class _WindowsConsoleReader(_WindowsConsoleRawIOBase): class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
def readable(self): def readable(self):
return True return True
@ -116,20 +136,26 @@ class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
if not bytes_to_be_read: if not bytes_to_be_read:
return 0 return 0
elif bytes_to_be_read % 2: elif bytes_to_be_read % 2:
raise ValueError('cannot read odd number of bytes from ' raise ValueError(
'UTF-16-LE encoded console') "cannot read odd number of bytes from UTF-16-LE encoded console"
)
buffer = get_buffer(b, writable=True) buffer = get_buffer(b, writable=True)
code_units_to_be_read = bytes_to_be_read // 2 code_units_to_be_read = bytes_to_be_read // 2
code_units_read = c_ulong() code_units_read = c_ulong()
rv = ReadConsoleW(self.handle, buffer, code_units_to_be_read, rv = ReadConsoleW(
byref(code_units_read), None) HANDLE(self.handle),
buffer,
code_units_to_be_read,
byref(code_units_read),
None,
)
if GetLastError() == ERROR_OPERATION_ABORTED: if GetLastError() == ERROR_OPERATION_ABORTED:
# wait for KeyboardInterrupt # wait for KeyboardInterrupt
time.sleep(0.1) time.sleep(0.1)
if not rv: if not rv:
raise OSError('Windows error: %s' % GetLastError()) raise OSError("Windows error: {}".format(GetLastError()))
if buffer[0] == EOF: if buffer[0] == EOF:
return 0 return 0
@ -137,27 +163,30 @@ class _WindowsConsoleReader(_WindowsConsoleRawIOBase):
class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
def writable(self): def writable(self):
return True return True
@staticmethod @staticmethod
def _get_error_message(errno): def _get_error_message(errno):
if errno == ERROR_SUCCESS: if errno == ERROR_SUCCESS:
return 'ERROR_SUCCESS' return "ERROR_SUCCESS"
elif errno == ERROR_NOT_ENOUGH_MEMORY: elif errno == ERROR_NOT_ENOUGH_MEMORY:
return 'ERROR_NOT_ENOUGH_MEMORY' return "ERROR_NOT_ENOUGH_MEMORY"
return 'Windows error %s' % errno return "Windows error {}".format(errno)
def write(self, b): def write(self, b):
bytes_to_be_written = len(b) bytes_to_be_written = len(b)
buf = get_buffer(b) buf = get_buffer(b)
code_units_to_be_written = min(bytes_to_be_written, code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2
MAX_BYTES_WRITTEN) // 2
code_units_written = c_ulong() code_units_written = c_ulong()
WriteConsoleW(self.handle, buf, code_units_to_be_written, WriteConsoleW(
byref(code_units_written), None) HANDLE(self.handle),
buf,
code_units_to_be_written,
byref(code_units_written),
None,
)
bytes_written = 2 * code_units_written.value bytes_written = 2 * code_units_written.value
if bytes_written == 0 and bytes_to_be_written > 0: if bytes_written == 0 and bytes_to_be_written > 0:
@ -166,7 +195,6 @@ class _WindowsConsoleWriter(_WindowsConsoleRawIOBase):
class ConsoleStream(object): class ConsoleStream(object):
def __init__(self, text_stream, byte_stream): def __init__(self, text_stream, byte_stream):
self._text_stream = text_stream self._text_stream = text_stream
self.buffer = byte_stream self.buffer = byte_stream
@ -195,9 +223,8 @@ class ConsoleStream(object):
return self.buffer.isatty() return self.buffer.isatty()
def __repr__(self): def __repr__(self):
return '<ConsoleStream name=%r encoding=%r>' % ( return "<ConsoleStream name={!r} encoding={!r}>".format(
self.name, self.name, self.encoding
self.encoding,
) )
@ -207,6 +234,7 @@ class WindowsChunkedWriter(object):
attribute access apart from method 'write()' which we wrap to write in attribute access apart from method 'write()' which we wrap to write in
limited chunks due to a Windows limitation on binary console streams. limited chunks due to a Windows limitation on binary console streams.
""" """
def __init__(self, wrapped): def __init__(self, wrapped):
# double-underscore everything to prevent clashes with names of # double-underscore everything to prevent clashes with names of
# attributes on the wrapped stream object. # attributes on the wrapped stream object.
@ -221,7 +249,7 @@ class WindowsChunkedWriter(object):
while written < total_to_write: while written < total_to_write:
to_write = min(total_to_write - written, MAX_BYTES_WRITTEN) to_write = min(total_to_write - written, MAX_BYTES_WRITTEN)
self.__wrapped.write(text[written:written+to_write]) self.__wrapped.write(text[written : written + to_write])
written += to_write written += to_write
@ -230,7 +258,11 @@ _wrapped_std_streams = set()
def _wrap_std_stream(name): def _wrap_std_stream(name):
# Python 2 & Windows 7 and below # Python 2 & Windows 7 and below
if PY2 and sys.getwindowsversion()[:2] <= (6, 1) and name not in _wrapped_std_streams: if (
PY2
and sys.getwindowsversion()[:2] <= (6, 1)
and name not in _wrapped_std_streams
):
setattr(sys, name, WindowsChunkedWriter(getattr(sys, name))) setattr(sys, name, WindowsChunkedWriter(getattr(sys, name)))
_wrapped_std_streams.add(name) _wrapped_std_streams.add(name)
@ -238,43 +270,59 @@ def _wrap_std_stream(name):
def _get_text_stdin(buffer_stream): def _get_text_stdin(buffer_stream):
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
'utf-16-le', 'strict', line_buffering=True) "utf-16-le",
"strict",
line_buffering=True,
)
return ConsoleStream(text_stream, buffer_stream) return ConsoleStream(text_stream, buffer_stream)
def _get_text_stdout(buffer_stream): def _get_text_stdout(buffer_stream):
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
'utf-16-le', 'strict', line_buffering=True) "utf-16-le",
"strict",
line_buffering=True,
)
return ConsoleStream(text_stream, buffer_stream) return ConsoleStream(text_stream, buffer_stream)
def _get_text_stderr(buffer_stream): def _get_text_stderr(buffer_stream):
text_stream = _NonClosingTextIOWrapper( text_stream = _NonClosingTextIOWrapper(
io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
'utf-16-le', 'strict', line_buffering=True) "utf-16-le",
"strict",
line_buffering=True,
)
return ConsoleStream(text_stream, buffer_stream) return ConsoleStream(text_stream, buffer_stream)
if PY2: if PY2:
def _hash_py_argv(): def _hash_py_argv():
return zlib.crc32('\x00'.join(sys.argv[1:])) return zlib.crc32("\x00".join(sys.argv[1:]))
_initial_argv_hash = _hash_py_argv() _initial_argv_hash = _hash_py_argv()
def _get_windows_argv(): def _get_windows_argv():
argc = c_int(0) argc = c_int(0)
argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc)) argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))
argv = [argv_unicode[i] for i in range(0, argc.value)] if not argv_unicode:
raise WinError()
try:
argv = [argv_unicode[i] for i in range(0, argc.value)]
finally:
LocalFree(argv_unicode)
del argv_unicode
if not hasattr(sys, 'frozen'): if not hasattr(sys, "frozen"):
argv = argv[1:] argv = argv[1:]
while len(argv) > 0: while len(argv) > 0:
arg = argv[0] arg = argv[0]
if not arg.startswith('-') or arg == '-': if not arg.startswith("-") or arg == "-":
break break
argv = argv[1:] argv = argv[1:]
if arg.startswith(('-c', '-m')): if arg.startswith(("-c", "-m")):
break break
return argv[1:] return argv[1:]
@ -287,15 +335,30 @@ _stream_factories = {
} }
def _is_console(f):
if not hasattr(f, "fileno"):
return False
try:
fileno = f.fileno()
except OSError:
return False
handle = msvcrt.get_osfhandle(fileno)
return bool(GetConsoleMode(handle, byref(DWORD())))
def _get_windows_console_stream(f, encoding, errors): def _get_windows_console_stream(f, encoding, errors):
if get_buffer is not None and \ if (
encoding in ('utf-16-le', None) \ get_buffer is not None
and errors in ('strict', None) and \ and encoding in ("utf-16-le", None)
hasattr(f, 'isatty') and f.isatty(): and errors in ("strict", None)
and _is_console(f)
):
func = _stream_factories.get(f.fileno()) func = _stream_factories.get(f.fileno())
if func is not None: if func is not None:
if not PY2: if not PY2:
f = getattr(f, 'buffer', None) f = getattr(f, "buffer", None)
if f is None: if f is None:
return None return None
else: else:

View file

@ -3,37 +3,51 @@ import inspect
import os import os
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
from itertools import repeat
from functools import update_wrapper from functools import update_wrapper
from itertools import repeat
from .types import convert_type, IntRange, BOOL from ._compat import isidentifier
from .utils import PacifyFlushWrapper, make_str, make_default_short_help, \ from ._compat import iteritems
echo, get_os_args from ._compat import PY2
from .exceptions import ClickException, UsageError, BadParameter, Abort, \ from ._compat import string_types
MissingParameter, Exit from ._unicodefun import _check_for_unicode_literals
from .termui import prompt, confirm, style from ._unicodefun import _verify_python3_env
from .formatting import HelpFormatter, join_options from .exceptions import Abort
from .parser import OptionParser, split_opt from .exceptions import BadParameter
from .globals import push_context, pop_context from .exceptions import ClickException
from .exceptions import Exit
from ._compat import PY2, isidentifier, iteritems, string_types from .exceptions import MissingParameter
from ._unicodefun import _check_for_unicode_literals, _verify_python3_env from .exceptions import UsageError
from .formatting import HelpFormatter
from .formatting import join_options
from .globals import pop_context
from .globals import push_context
from .parser import OptionParser
from .parser import split_opt
from .termui import confirm
from .termui import prompt
from .termui import style
from .types import BOOL
from .types import convert_type
from .types import IntRange
from .utils import echo
from .utils import get_os_args
from .utils import make_default_short_help
from .utils import make_str
from .utils import PacifyFlushWrapper
_missing = object() _missing = object()
SUBCOMMAND_METAVAR = "COMMAND [ARGS]..."
SUBCOMMANDS_METAVAR = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."
SUBCOMMAND_METAVAR = 'COMMAND [ARGS]...' DEPRECATED_HELP_NOTICE = " (DEPRECATED)"
SUBCOMMANDS_METAVAR = 'COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...' DEPRECATED_INVOKE_NOTICE = "DeprecationWarning: The command %(name)s is deprecated."
DEPRECATED_HELP_NOTICE = ' (DEPRECATED)'
DEPRECATED_INVOKE_NOTICE = 'DeprecationWarning: ' + \
'The command %(name)s is deprecated.'
def _maybe_show_deprecated_notice(cmd): def _maybe_show_deprecated_notice(cmd):
if cmd.deprecated: if cmd.deprecated:
echo(style(DEPRECATED_INVOKE_NOTICE % {'name': cmd.name}, fg='red'), err=True) echo(style(DEPRECATED_INVOKE_NOTICE % {"name": cmd.name}, fg="red"), err=True)
def fast_exit(code): def fast_exit(code):
@ -48,12 +62,13 @@ def fast_exit(code):
def _bashcomplete(cmd, prog_name, complete_var=None): def _bashcomplete(cmd, prog_name, complete_var=None):
"""Internal handler for the bash completion support.""" """Internal handler for the bash completion support."""
if complete_var is None: if complete_var is None:
complete_var = '_%s_COMPLETE' % (prog_name.replace('-', '_')).upper() complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
complete_instr = os.environ.get(complete_var) complete_instr = os.environ.get(complete_var)
if not complete_instr: if not complete_instr:
return return
from ._bashcomplete import bashcomplete from ._bashcomplete import bashcomplete
if bashcomplete(cmd, prog_name, complete_var, complete_instr): if bashcomplete(cmd, prog_name, complete_var, complete_instr):
fast_exit(1) fast_exit(1)
@ -62,19 +77,28 @@ def _check_multicommand(base_command, cmd_name, cmd, register=False):
if not base_command.chain or not isinstance(cmd, MultiCommand): if not base_command.chain or not isinstance(cmd, MultiCommand):
return return
if register: if register:
hint = 'It is not possible to add multi commands as children to ' \ hint = (
'another multi command that is in chain mode' "It is not possible to add multi commands as children to"
" another multi command that is in chain mode."
)
else: else:
hint = 'Found a multi command as subcommand to a multi command ' \ hint = (
'that is in chain mode. This is not supported' "Found a multi command as subcommand to a multi command"
raise RuntimeError('%s. Command "%s" is set to chain and "%s" was ' " that is in chain mode. This is not supported."
'added as subcommand but it in itself is a ' )
'multi command. ("%s" is a %s within a chained ' raise RuntimeError(
'%s named "%s").' % ( "{}. Command '{}' is set to chain and '{}' was added as"
hint, base_command.name, cmd_name, " subcommand but it in itself is a multi command. ('{}' is a {}"
cmd_name, cmd.__class__.__name__, " within a chained {} named '{}').".format(
base_command.__class__.__name__, hint,
base_command.name)) base_command.name,
cmd_name,
cmd_name,
cmd.__class__.__name__,
base_command.__class__.__name__,
base_command.name,
)
)
def batch(iterable, batch_size): def batch(iterable, batch_size):
@ -82,25 +106,26 @@ def batch(iterable, batch_size):
def invoke_param_callback(callback, ctx, param, value): def invoke_param_callback(callback, ctx, param, value):
code = getattr(callback, '__code__', None) code = getattr(callback, "__code__", None)
args = getattr(code, 'co_argcount', 3) args = getattr(code, "co_argcount", 3)
if args < 3: if args < 3:
# This will become a warning in Click 3.0:
from warnings import warn from warnings import warn
warn(Warning('Invoked legacy parameter callback "%s". The new '
'signature for such callbacks starting with ' warn(
'click 2.0 is (ctx, param, value).' "Parameter callbacks take 3 args, (ctx, param, value). The"
% callback), stacklevel=3) " 2-arg style is deprecated and will be removed in 8.0.".format(callback),
DeprecationWarning,
stacklevel=3,
)
return callback(ctx, value) return callback(ctx, value)
return callback(ctx, param, value) return callback(ctx, param, value)
@contextmanager @contextmanager
def augment_usage_errors(ctx, param=None): def augment_usage_errors(ctx, param=None):
"""Context manager that attaches extra information to exceptions that """Context manager that attaches extra information to exceptions."""
fly.
"""
try: try:
yield yield
except BadParameter as e: except BadParameter as e:
@ -120,11 +145,12 @@ def iter_params_for_processing(invocation_order, declaration_order):
for processing and an iterable of parameters that exist, this returns for processing and an iterable of parameters that exist, this returns
a list in the correct order as they should be processed. a list in the correct order as they should be processed.
""" """
def sort_key(item): def sort_key(item):
try: try:
idx = invocation_order.index(item) idx = invocation_order.index(item)
except ValueError: except ValueError:
idx = float('inf') idx = float("inf")
return (not item.is_eager, idx) return (not item.is_eager, idx)
return sorted(declaration_order, key=sort_key) return sorted(declaration_order, key=sort_key)
@ -154,6 +180,9 @@ class Context(object):
Added the `color`, `ignore_unknown_options`, and Added the `color`, `ignore_unknown_options`, and
`max_content_width` parameters. `max_content_width` parameters.
.. versionadded:: 7.1
Added the `show_default` parameter.
:param command: the command class for this context. :param command: the command class for this context.
:param parent: the parent context. :param parent: the parent context.
:param info_name: the info name for this invocation. Generally this :param info_name: the info name for this invocation. Generally this
@ -208,15 +237,30 @@ class Context(object):
codes are used in texts that Click prints which is by codes are used in texts that Click prints which is by
default not the case. This for instance would affect default not the case. This for instance would affect
help output. help output.
:param show_default: if True, shows defaults for all options.
Even if an option is later created with show_default=False,
this command-level setting overrides it.
""" """
def __init__(self, command, parent=None, info_name=None, obj=None, def __init__(
auto_envvar_prefix=None, default_map=None, self,
terminal_width=None, max_content_width=None, command,
resilient_parsing=False, allow_extra_args=None, parent=None,
allow_interspersed_args=None, info_name=None,
ignore_unknown_options=None, help_option_names=None, obj=None,
token_normalize_func=None, color=None): auto_envvar_prefix=None,
default_map=None,
terminal_width=None,
max_content_width=None,
resilient_parsing=False,
allow_extra_args=None,
allow_interspersed_args=None,
ignore_unknown_options=None,
help_option_names=None,
token_normalize_func=None,
color=None,
show_default=None,
):
#: the parent context or `None` if none exists. #: the parent context or `None` if none exists.
self.parent = parent self.parent = parent
#: the :class:`Command` for this context. #: the :class:`Command` for this context.
@ -237,12 +281,14 @@ class Context(object):
obj = parent.obj obj = parent.obj
#: the user object stored. #: the user object stored.
self.obj = obj self.obj = obj
self._meta = getattr(parent, 'meta', {}) self._meta = getattr(parent, "meta", {})
#: A dictionary (-like object) with defaults for parameters. #: A dictionary (-like object) with defaults for parameters.
if default_map is None \ if (
and parent is not None \ default_map is None
and parent.default_map is not None: and parent is not None
and parent.default_map is not None
):
default_map = parent.default_map.get(info_name) default_map = parent.default_map.get(info_name)
self.default_map = default_map self.default_map = default_map
@ -301,7 +347,7 @@ class Context(object):
if parent is not None: if parent is not None:
help_option_names = parent.help_option_names help_option_names = parent.help_option_names
else: else:
help_option_names = ['--help'] help_option_names = ["--help"]
#: The names for the help options. #: The names for the help options.
self.help_option_names = help_option_names self.help_option_names = help_option_names
@ -322,13 +368,18 @@ class Context(object):
# the command on this level has a name, we can expand the envvar # the command on this level has a name, we can expand the envvar
# prefix automatically. # prefix automatically.
if auto_envvar_prefix is None: if auto_envvar_prefix is None:
if parent is not None \ if (
and parent.auto_envvar_prefix is not None and \ parent is not None
self.info_name is not None: and parent.auto_envvar_prefix is not None
auto_envvar_prefix = '%s_%s' % (parent.auto_envvar_prefix, and self.info_name is not None
self.info_name.upper()) ):
auto_envvar_prefix = "{}_{}".format(
parent.auto_envvar_prefix, self.info_name.upper()
)
else: else:
auto_envvar_prefix = auto_envvar_prefix.upper() auto_envvar_prefix = auto_envvar_prefix.upper()
if auto_envvar_prefix is not None:
auto_envvar_prefix = auto_envvar_prefix.replace("-", "_")
self.auto_envvar_prefix = auto_envvar_prefix self.auto_envvar_prefix = auto_envvar_prefix
if color is None and parent is not None: if color is None and parent is not None:
@ -337,6 +388,8 @@ class Context(object):
#: Controls if styling output is wanted or not. #: Controls if styling output is wanted or not.
self.color = color self.color = color
self.show_default = show_default
self._close_callbacks = [] self._close_callbacks = []
self._depth = 0 self._depth = 0
@ -404,7 +457,7 @@ class Context(object):
Example usage:: Example usage::
LANG_KEY = __name__ + '.lang' LANG_KEY = f'{__name__}.lang'
def set_language(value): def set_language(value):
ctx = get_current_context() ctx = get_current_context()
@ -419,8 +472,9 @@ class Context(object):
def make_formatter(self): def make_formatter(self):
"""Creates the formatter for the help and usage output.""" """Creates the formatter for the help and usage output."""
return HelpFormatter(width=self.terminal_width, return HelpFormatter(
max_width=self.max_content_width) width=self.terminal_width, max_width=self.max_content_width
)
def call_on_close(self, f): def call_on_close(self, f):
"""This decorator remembers a function as callback that should be """This decorator remembers a function as callback that should be
@ -446,11 +500,11 @@ class Context(object):
information on the help page. It's automatically created by information on the help page. It's automatically created by
combining the info names of the chain of contexts to the root. combining the info names of the chain of contexts to the root.
""" """
rv = '' rv = ""
if self.info_name is not None: if self.info_name is not None:
rv = self.info_name rv = self.info_name
if self.parent is not None: if self.parent is not None:
rv = self.parent.command_path + ' ' + rv rv = "{} {}".format(self.parent.command_path, rv)
return rv.lstrip() return rv.lstrip()
def find_root(self): def find_root(self):
@ -515,7 +569,7 @@ class Context(object):
""" """
return self.command.get_help(self) return self.command.get_help(self)
def invoke(*args, **kwargs): def invoke(*args, **kwargs): # noqa: B902
"""Invokes a command callback in exactly the way it expects. There """Invokes a command callback in exactly the way it expects. There
are two ways to invoke this method: are two ways to invoke this method:
@ -542,8 +596,9 @@ class Context(object):
callback = other_cmd.callback callback = other_cmd.callback
ctx = Context(other_cmd, info_name=other_cmd.name, parent=self) ctx = Context(other_cmd, info_name=other_cmd.name, parent=self)
if callback is None: if callback is None:
raise TypeError('The given command does not have a ' raise TypeError(
'callback that can be invoked.') "The given command does not have a callback that can be invoked."
)
for param in other_cmd.params: for param in other_cmd.params:
if param.name not in kwargs and param.expose_value: if param.name not in kwargs and param.expose_value:
@ -554,7 +609,7 @@ class Context(object):
with ctx: with ctx:
return callback(*args, **kwargs) return callback(*args, **kwargs)
def forward(*args, **kwargs): def forward(*args, **kwargs): # noqa: B902
"""Similar to :meth:`invoke` but fills in default keyword """Similar to :meth:`invoke` but fills in default keyword
arguments from the current context if the other command expects arguments from the current context if the other command expects
it. This cannot invoke callbacks directly, only other commands. it. This cannot invoke callbacks directly, only other commands.
@ -564,7 +619,7 @@ class Context(object):
# It's also possible to invoke another command which might or # It's also possible to invoke another command which might or
# might not have a callback. # might not have a callback.
if not isinstance(cmd, Command): if not isinstance(cmd, Command):
raise TypeError('Callback is not a command.') raise TypeError("Callback is not a command.")
for param in self.params: for param in self.params:
if param not in kwargs: if param not in kwargs:
@ -594,6 +649,7 @@ class BaseCommand(object):
:param context_settings: an optional dictionary with defaults that are :param context_settings: an optional dictionary with defaults that are
passed to the context object. passed to the context object.
""" """
#: the default for the :attr:`Context.allow_extra_args` flag. #: the default for the :attr:`Context.allow_extra_args` flag.
allow_extra_args = False allow_extra_args = False
#: the default for the :attr:`Context.allow_interspersed_args` flag. #: the default for the :attr:`Context.allow_interspersed_args` flag.
@ -612,11 +668,14 @@ class BaseCommand(object):
#: an optional dictionary with defaults passed to the context. #: an optional dictionary with defaults passed to the context.
self.context_settings = context_settings self.context_settings = context_settings
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, self.name)
def get_usage(self, ctx): def get_usage(self, ctx):
raise NotImplementedError('Base commands cannot get usage') raise NotImplementedError("Base commands cannot get usage")
def get_help(self, ctx): def get_help(self, ctx):
raise NotImplementedError('Base commands cannot get help') raise NotImplementedError("Base commands cannot get help")
def make_context(self, info_name, args, parent=None, **extra): def make_context(self, info_name, args, parent=None, **extra):
"""This function when given an info name and arguments will kick """This function when given an info name and arguments will kick
@ -646,17 +705,22 @@ class BaseCommand(object):
and parses the arguments, then modifies the context as necessary. and parses the arguments, then modifies the context as necessary.
This is automatically invoked by :meth:`make_context`. This is automatically invoked by :meth:`make_context`.
""" """
raise NotImplementedError('Base commands do not know how to parse ' raise NotImplementedError("Base commands do not know how to parse arguments.")
'arguments.')
def invoke(self, ctx): def invoke(self, ctx):
"""Given a context, this invokes the command. The default """Given a context, this invokes the command. The default
implementation is raising a not implemented error. implementation is raising a not implemented error.
""" """
raise NotImplementedError('Base commands are not invokable by default') raise NotImplementedError("Base commands are not invokable by default")
def main(self, args=None, prog_name=None, complete_var=None, def main(
standalone_mode=True, **extra): self,
args=None,
prog_name=None,
complete_var=None,
standalone_mode=True,
**extra
):
"""This is the way to invoke a script with all the bells and """This is the way to invoke a script with all the bells and
whistles as a command line application. This will always terminate whistles as a command line application. This will always terminate
the application after a call. If this is not wanted, ``SystemExit`` the application after a call. If this is not wanted, ``SystemExit``
@ -703,8 +767,9 @@ class BaseCommand(object):
args = list(args) args = list(args)
if prog_name is None: if prog_name is None:
prog_name = make_str(os.path.basename( prog_name = make_str(
sys.argv and sys.argv[0] or __file__)) os.path.basename(sys.argv[0] if sys.argv else __file__)
)
# Hook for the Bash completion. This only activates if the Bash # Hook for the Bash completion. This only activates if the Bash
# completion is actually enabled, otherwise this is quite a fast # completion is actually enabled, otherwise this is quite a fast
@ -756,7 +821,7 @@ class BaseCommand(object):
except Abort: except Abort:
if not standalone_mode: if not standalone_mode:
raise raise
echo('Aborted!', file=sys.stderr) echo("Aborted!", file=sys.stderr)
sys.exit(1) sys.exit(1)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
@ -771,6 +836,8 @@ class Command(BaseCommand):
.. versionchanged:: 2.0 .. versionchanged:: 2.0
Added the `context_settings` parameter. Added the `context_settings` parameter.
.. versionchanged:: 7.1
Added the `no_args_is_help` parameter.
:param name: the name of the command to use unless a group overrides it. :param name: the name of the command to use unless a group overrides it.
:param context_settings: an optional dictionary with defaults that are :param context_settings: an optional dictionary with defaults that are
@ -785,16 +852,31 @@ class Command(BaseCommand):
shown on the command listing of the parent command. shown on the command listing of the parent command.
:param add_help_option: by default each command registers a ``--help`` :param add_help_option: by default each command registers a ``--help``
option. This can be disabled by this parameter. option. This can be disabled by this parameter.
:param no_args_is_help: this controls what happens if no arguments are
provided. This option is disabled by default.
If enabled this will add ``--help`` as argument
if no arguments are passed
:param hidden: hide this command from help outputs. :param hidden: hide this command from help outputs.
:param deprecated: issues a message indicating that :param deprecated: issues a message indicating that
the command is deprecated. the command is deprecated.
""" """
def __init__(self, name, context_settings=None, callback=None, def __init__(
params=None, help=None, epilog=None, short_help=None, self,
options_metavar='[OPTIONS]', add_help_option=True, name,
hidden=False, deprecated=False): context_settings=None,
callback=None,
params=None,
help=None,
epilog=None,
short_help=None,
options_metavar="[OPTIONS]",
add_help_option=True,
no_args_is_help=False,
hidden=False,
deprecated=False,
):
BaseCommand.__init__(self, name, context_settings) BaseCommand.__init__(self, name, context_settings)
#: the callback to execute when the command fires. This might be #: the callback to execute when the command fires. This might be
#: `None` in which case nothing happens. #: `None` in which case nothing happens.
@ -805,20 +887,25 @@ class Command(BaseCommand):
self.params = params or [] self.params = params or []
# if a form feed (page break) is found in the help text, truncate help # if a form feed (page break) is found in the help text, truncate help
# text to the content preceding the first form feed # text to the content preceding the first form feed
if help and '\f' in help: if help and "\f" in help:
help = help.split('\f', 1)[0] help = help.split("\f", 1)[0]
self.help = help self.help = help
self.epilog = epilog self.epilog = epilog
self.options_metavar = options_metavar self.options_metavar = options_metavar
self.short_help = short_help self.short_help = short_help
self.add_help_option = add_help_option self.add_help_option = add_help_option
self.no_args_is_help = no_args_is_help
self.hidden = hidden self.hidden = hidden
self.deprecated = deprecated self.deprecated = deprecated
def get_usage(self, ctx): def get_usage(self, ctx):
"""Formats the usage line into a string and returns it.
Calls :meth:`format_usage` internally.
"""
formatter = ctx.make_formatter() formatter = ctx.make_formatter()
self.format_usage(ctx, formatter) self.format_usage(ctx, formatter)
return formatter.getvalue().rstrip('\n') return formatter.getvalue().rstrip("\n")
def get_params(self, ctx): def get_params(self, ctx):
rv = self.params rv = self.params
@ -828,9 +915,12 @@ class Command(BaseCommand):
return rv return rv
def format_usage(self, ctx, formatter): def format_usage(self, ctx, formatter):
"""Writes the usage line into the formatter.""" """Writes the usage line into the formatter.
This is a low-level method called by :meth:`get_usage`.
"""
pieces = self.collect_usage_pieces(ctx) pieces = self.collect_usage_pieces(ctx)
formatter.write_usage(ctx.command_path, ' '.join(pieces)) formatter.write_usage(ctx.command_path, " ".join(pieces))
def collect_usage_pieces(self, ctx): def collect_usage_pieces(self, ctx):
"""Returns all the pieces that go into the usage line and returns """Returns all the pieces that go into the usage line and returns
@ -859,10 +949,15 @@ class Command(BaseCommand):
if value and not ctx.resilient_parsing: if value and not ctx.resilient_parsing:
echo(ctx.get_help(), color=ctx.color) echo(ctx.get_help(), color=ctx.color)
ctx.exit() ctx.exit()
return Option(help_options, is_flag=True,
is_eager=True, expose_value=False, return Option(
callback=show_help, help_options,
help='Show this message and exit.') is_flag=True,
is_eager=True,
expose_value=False,
callback=show_help,
help="Show this message and exit.",
)
def make_parser(self, ctx): def make_parser(self, ctx):
"""Creates the underlying option parser for this command.""" """Creates the underlying option parser for this command."""
@ -872,21 +967,31 @@ class Command(BaseCommand):
return parser return parser
def get_help(self, ctx): def get_help(self, ctx):
"""Formats the help into a string and returns it. This creates a """Formats the help into a string and returns it.
formatter and will call into the following formatting methods:
Calls :meth:`format_help` internally.
""" """
formatter = ctx.make_formatter() formatter = ctx.make_formatter()
self.format_help(ctx, formatter) self.format_help(ctx, formatter)
return formatter.getvalue().rstrip('\n') return formatter.getvalue().rstrip("\n")
def get_short_help_str(self, limit=45): def get_short_help_str(self, limit=45):
"""Gets short help for the command or makes it by shortening the long help string.""" """Gets short help for the command or makes it by shortening the
return self.short_help or self.help and make_default_short_help(self.help, limit) or '' long help string.
"""
return (
self.short_help
or self.help
and make_default_short_help(self.help, limit)
or ""
)
def format_help(self, ctx, formatter): def format_help(self, ctx, formatter):
"""Writes the help into the formatter if it exists. """Writes the help into the formatter if it exists.
This calls into the following methods: This is a low-level method called by :meth:`get_help`.
This calls the following methods:
- :meth:`format_usage` - :meth:`format_usage`
- :meth:`format_help_text` - :meth:`format_help_text`
@ -921,7 +1026,7 @@ class Command(BaseCommand):
opts.append(rv) opts.append(rv)
if opts: if opts:
with formatter.section('Options'): with formatter.section("Options"):
formatter.write_dl(opts) formatter.write_dl(opts)
def format_epilog(self, ctx, formatter): def format_epilog(self, ctx, formatter):
@ -932,17 +1037,22 @@ class Command(BaseCommand):
formatter.write_text(self.epilog) formatter.write_text(self.epilog)
def parse_args(self, ctx, args): def parse_args(self, ctx, args):
if not args and self.no_args_is_help and not ctx.resilient_parsing:
echo(ctx.get_help(), color=ctx.color)
ctx.exit()
parser = self.make_parser(ctx) parser = self.make_parser(ctx)
opts, args, param_order = parser.parse_args(args=args) opts, args, param_order = parser.parse_args(args=args)
for param in iter_params_for_processing( for param in iter_params_for_processing(param_order, self.get_params(ctx)):
param_order, self.get_params(ctx)):
value, args = param.handle_parse_result(ctx, opts, args) value, args = param.handle_parse_result(ctx, opts, args)
if args and not ctx.allow_extra_args and not ctx.resilient_parsing: if args and not ctx.allow_extra_args and not ctx.resilient_parsing:
ctx.fail('Got unexpected extra argument%s (%s)' ctx.fail(
% (len(args) != 1 and 's' or '', "Got unexpected extra argument{} ({})".format(
' '.join(map(make_str, args)))) "s" if len(args) != 1 else "", " ".join(map(make_str, args))
)
)
ctx.args = args ctx.args = args
return args return args
@ -979,12 +1089,20 @@ class MultiCommand(Command):
:param result_callback: the result callback to attach to this multi :param result_callback: the result callback to attach to this multi
command. command.
""" """
allow_extra_args = True allow_extra_args = True
allow_interspersed_args = False allow_interspersed_args = False
def __init__(self, name=None, invoke_without_command=False, def __init__(
no_args_is_help=None, subcommand_metavar=None, self,
chain=False, result_callback=None, **attrs): name=None,
invoke_without_command=False,
no_args_is_help=None,
subcommand_metavar=None,
chain=False,
result_callback=None,
**attrs
):
Command.__init__(self, name, **attrs) Command.__init__(self, name, **attrs)
if no_args_is_help is None: if no_args_is_help is None:
no_args_is_help = not invoke_without_command no_args_is_help = not invoke_without_command
@ -1004,8 +1122,10 @@ class MultiCommand(Command):
if self.chain: if self.chain:
for param in self.params: for param in self.params:
if isinstance(param, Argument) and not param.required: if isinstance(param, Argument) and not param.required:
raise RuntimeError('Multi commands in chain mode cannot ' raise RuntimeError(
'have optional arguments.') "Multi commands in chain mode cannot have"
" optional arguments."
)
def collect_usage_pieces(self, ctx): def collect_usage_pieces(self, ctx):
rv = Command.collect_usage_pieces(self, ctx) rv = Command.collect_usage_pieces(self, ctx)
@ -1041,16 +1161,19 @@ class MultiCommand(Command):
:param replace: if set to `True` an already existing result :param replace: if set to `True` an already existing result
callback will be removed. callback will be removed.
""" """
def decorator(f): def decorator(f):
old_callback = self.result_callback old_callback = self.result_callback
if old_callback is None or replace: if old_callback is None or replace:
self.result_callback = f self.result_callback = f
return f return f
def function(__value, *args, **kwargs): def function(__value, *args, **kwargs):
return f(old_callback(__value, *args, **kwargs), return f(old_callback(__value, *args, **kwargs), *args, **kwargs)
*args, **kwargs)
self.result_callback = rv = update_wrapper(function, f) self.result_callback = rv = update_wrapper(function, f)
return rv return rv
return decorator return decorator
def format_commands(self, ctx, formatter): def format_commands(self, ctx, formatter):
@ -1078,7 +1201,7 @@ class MultiCommand(Command):
rows.append((subcommand, help)) rows.append((subcommand, help))
if rows: if rows:
with formatter.section('Commands'): with formatter.section("Commands"):
formatter.write_dl(rows) formatter.write_dl(rows)
def parse_args(self, ctx, args): def parse_args(self, ctx, args):
@ -1098,8 +1221,7 @@ class MultiCommand(Command):
def invoke(self, ctx): def invoke(self, ctx):
def _process_result(value): def _process_result(value):
if self.result_callback is not None: if self.result_callback is not None:
value = ctx.invoke(self.result_callback, value, value = ctx.invoke(self.result_callback, value, **ctx.params)
**ctx.params)
return value return value
if not ctx.protected_args: if not ctx.protected_args:
@ -1115,7 +1237,7 @@ class MultiCommand(Command):
with ctx: with ctx:
Command.invoke(self, ctx) Command.invoke(self, ctx)
return _process_result([]) return _process_result([])
ctx.fail('Missing command.') ctx.fail("Missing command.")
# Fetch args back out # Fetch args back out
args = ctx.protected_args + ctx.args args = ctx.protected_args + ctx.args
@ -1142,7 +1264,7 @@ class MultiCommand(Command):
# set to ``*`` to inform the command that subcommands are executed # set to ``*`` to inform the command that subcommands are executed
# but nothing else. # but nothing else.
with ctx: with ctx:
ctx.invoked_subcommand = args and '*' or None ctx.invoked_subcommand = "*" if args else None
Command.invoke(self, ctx) Command.invoke(self, ctx)
# Otherwise we make every single context and invoke them in a # Otherwise we make every single context and invoke them in a
@ -1151,9 +1273,13 @@ class MultiCommand(Command):
contexts = [] contexts = []
while args: while args:
cmd_name, cmd, args = self.resolve_command(ctx, args) cmd_name, cmd, args = self.resolve_command(ctx, args)
sub_ctx = cmd.make_context(cmd_name, args, parent=ctx, sub_ctx = cmd.make_context(
allow_extra_args=True, cmd_name,
allow_interspersed_args=False) args,
parent=ctx,
allow_extra_args=True,
allow_interspersed_args=False,
)
contexts.append(sub_ctx) contexts.append(sub_ctx)
args, sub_ctx.args = sub_ctx.args, [] args, sub_ctx.args = sub_ctx.args, []
@ -1185,7 +1311,7 @@ class MultiCommand(Command):
if cmd is None and not ctx.resilient_parsing: if cmd is None and not ctx.resilient_parsing:
if split_opt(cmd_name)[0]: if split_opt(cmd_name)[0]:
self.parse_args(ctx, ctx.args) self.parse_args(ctx, ctx.args)
ctx.fail('No such command "%s".' % original_cmd_name) ctx.fail("No such command '{}'.".format(original_cmd_name))
return cmd_name, cmd, args[1:] return cmd_name, cmd, args[1:]
@ -1220,7 +1346,7 @@ class Group(MultiCommand):
""" """
name = name or cmd.name name = name or cmd.name
if name is None: if name is None:
raise TypeError('Command has no name.') raise TypeError("Command has no name.")
_check_multicommand(self, name, cmd, register=True) _check_multicommand(self, name, cmd, register=True)
self.commands[name] = cmd self.commands[name] = cmd
@ -1230,10 +1356,13 @@ class Group(MultiCommand):
immediately registers the created command with this instance by immediately registers the created command with this instance by
calling into :meth:`add_command`. calling into :meth:`add_command`.
""" """
from .decorators import command
def decorator(f): def decorator(f):
cmd = command(*args, **kwargs)(f) cmd = command(*args, **kwargs)(f)
self.add_command(cmd) self.add_command(cmd)
return cmd return cmd
return decorator return decorator
def group(self, *args, **kwargs): def group(self, *args, **kwargs):
@ -1242,10 +1371,13 @@ class Group(MultiCommand):
immediately registers the created command with this instance by immediately registers the created command with this instance by
calling into :meth:`add_command`. calling into :meth:`add_command`.
""" """
from .decorators import group
def decorator(f): def decorator(f):
cmd = group(*args, **kwargs)(f) cmd = group(*args, **kwargs)(f)
self.add_command(cmd) self.add_command(cmd)
return cmd return cmd
return decorator return decorator
def get_command(self, ctx, cmd_name): def get_command(self, ctx, cmd_name):
@ -1294,12 +1426,6 @@ class Parameter(object):
Some settings are supported by both options and arguments. Some settings are supported by both options and arguments.
.. versionchanged:: 2.0
Changed signature for parameter callback to also be passed the
parameter. In Click 2.0, the old callback format will still work,
but it will raise a warning to give you change to migrate the
code easier.
:param param_decls: the parameter declarations for this option or :param param_decls: the parameter declarations for this option or
argument. This is a list of flags or argument argument. This is a list of flags or argument
names. names.
@ -1312,8 +1438,7 @@ class Parameter(object):
without any arguments. without any arguments.
:param callback: a callback that should be executed after the parameter :param callback: a callback that should be executed after the parameter
was matched. This is called as ``fn(ctx, param, was matched. This is called as ``fn(ctx, param,
value)`` and needs to return the value. Before Click value)`` and needs to return the value.
2.0, the signature was ``(ctx, value)``.
:param nargs: the number of arguments to match. If not ``1`` the return :param nargs: the number of arguments to match. If not ``1`` the return
value is a tuple instead of single value. The default for value is a tuple instead of single value. The default for
nargs is ``1`` (except if the type is a tuple, then it's nargs is ``1`` (except if the type is a tuple, then it's
@ -1327,15 +1452,36 @@ class Parameter(object):
order of processing. order of processing.
:param envvar: a string or list of strings that are environment variables :param envvar: a string or list of strings that are environment variables
that should be checked. that should be checked.
"""
param_type_name = 'parameter'
def __init__(self, param_decls=None, type=None, required=False, .. versionchanged:: 7.1
default=None, callback=None, nargs=None, metavar=None, Empty environment variables are ignored rather than taking the
expose_value=True, is_eager=False, envvar=None, empty string value. This makes it possible for scripts to clear
autocompletion=None): variables if they can't unset them.
self.name, self.opts, self.secondary_opts = \
self._parse_decls(param_decls or (), expose_value) .. versionchanged:: 2.0
Changed signature for parameter callback to also be passed the
parameter. The old callback format will still work, but it will
raise a warning to give you a chance to migrate the code easier.
"""
param_type_name = "parameter"
def __init__(
self,
param_decls=None,
type=None,
required=False,
default=None,
callback=None,
nargs=None,
metavar=None,
expose_value=True,
is_eager=False,
envvar=None,
autocompletion=None,
):
self.name, self.opts, self.secondary_opts = self._parse_decls(
param_decls or (), expose_value
)
self.type = convert_type(type, default) self.type = convert_type(type, default)
@ -1358,6 +1504,9 @@ class Parameter(object):
self.envvar = envvar self.envvar = envvar
self.autocompletion = autocompletion self.autocompletion = autocompletion
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, self.name)
@property @property
def human_readable_name(self): def human_readable_name(self):
"""Returns the human readable name of this parameter. This is the """Returns the human readable name of this parameter. This is the
@ -1372,7 +1521,7 @@ class Parameter(object):
if metavar is None: if metavar is None:
metavar = self.type.name.upper() metavar = self.type.name.upper()
if self.nargs != 1: if self.nargs != 1:
metavar += '...' metavar += "..."
return metavar return metavar
def get_default(self, ctx): def get_default(self, ctx):
@ -1402,10 +1551,11 @@ class Parameter(object):
""" """
if self.type.is_composite: if self.type.is_composite:
if self.nargs <= 1: if self.nargs <= 1:
raise TypeError('Attempted to invoke composite type ' raise TypeError(
'but nargs has been set to %s. This is ' "Attempted to invoke composite type but nargs has"
'not supported; nargs needs to be set to ' " been set to {}. This is not supported; nargs"
'a fixed value > 1.' % self.nargs) " needs to be set to a fixed value > 1.".format(self.nargs)
)
if self.multiple: if self.multiple:
return tuple(self.type(x or (), self, ctx) for x in value or ()) return tuple(self.type(x or (), self, ctx) for x in value or ())
return self.type(value or (), self, ctx) return self.type(value or (), self, ctx)
@ -1414,6 +1564,7 @@ class Parameter(object):
if level == 0: if level == 0:
return self.type(value, self, ctx) return self.type(value, self, ctx)
return tuple(_convert(x, level - 1) for x in value or ()) return tuple(_convert(x, level - 1) for x in value or ())
return _convert(value, (self.nargs != 1) + bool(self.multiple)) return _convert(value, (self.nargs != 1) + bool(self.multiple))
def process_value(self, ctx, value): def process_value(self, ctx, value):
@ -1454,7 +1605,10 @@ class Parameter(object):
if rv is not None: if rv is not None:
return rv return rv
else: else:
return os.environ.get(self.envvar) rv = os.environ.get(self.envvar)
if rv != "":
return rv
def value_from_envvar(self, ctx): def value_from_envvar(self, ctx):
rv = self.resolve_envvar_value(ctx) rv = self.resolve_envvar_value(ctx)
@ -1473,8 +1627,7 @@ class Parameter(object):
value = None value = None
if self.callback is not None: if self.callback is not None:
try: try:
value = invoke_param_callback( value = invoke_param_callback(self.callback, ctx, self, value)
self.callback, ctx, self, value)
except Exception: except Exception:
if not ctx.resilient_parsing: if not ctx.resilient_parsing:
raise raise
@ -1494,7 +1647,7 @@ class Parameter(object):
indicate which param caused the error. indicate which param caused the error.
""" """
hint_list = self.opts or [self.human_readable_name] hint_list = self.opts or [self.human_readable_name]
return ' / '.join('"%s"' % x for x in hint_list) return " / ".join(repr(x) for x in hint_list)
class Option(Parameter): class Option(Parameter):
@ -1535,19 +1688,33 @@ class Option(Parameter):
:param help: the help string. :param help: the help string.
:param hidden: hide this option from help outputs. :param hidden: hide this option from help outputs.
""" """
param_type_name = 'option'
def __init__(self, param_decls=None, show_default=False, param_type_name = "option"
prompt=False, confirmation_prompt=False,
hide_input=False, is_flag=None, flag_value=None, def __init__(
multiple=False, count=False, allow_from_autoenv=True, self,
type=None, help=None, hidden=False, show_choices=True, param_decls=None,
show_envvar=False, **attrs): show_default=False,
default_is_missing = attrs.get('default', _missing) is _missing prompt=False,
confirmation_prompt=False,
hide_input=False,
is_flag=None,
flag_value=None,
multiple=False,
count=False,
allow_from_autoenv=True,
type=None,
help=None,
hidden=False,
show_choices=True,
show_envvar=False,
**attrs
):
default_is_missing = attrs.get("default", _missing) is _missing
Parameter.__init__(self, param_decls, type=type, **attrs) Parameter.__init__(self, param_decls, type=type, **attrs)
if prompt is True: if prompt is True:
prompt_text = self.name.replace('_', ' ').capitalize() prompt_text = self.name.replace("_", " ").capitalize()
elif prompt is False: elif prompt is False:
prompt_text = None prompt_text = None
else: else:
@ -1569,8 +1736,7 @@ class Option(Parameter):
flag_value = not self.default flag_value = not self.default
self.is_flag = is_flag self.is_flag = is_flag
self.flag_value = flag_value self.flag_value = flag_value
if self.is_flag and isinstance(self.flag_value, bool) \ if self.is_flag and isinstance(self.flag_value, bool) and type in [None, bool]:
and type is None:
self.type = BOOL self.type = BOOL
self.is_bool_flag = True self.is_bool_flag = True
else: else:
@ -1594,22 +1760,22 @@ class Option(Parameter):
# Sanity check for stuff we don't support # Sanity check for stuff we don't support
if __debug__: if __debug__:
if self.nargs < 0: if self.nargs < 0:
raise TypeError('Options cannot have nargs < 0') raise TypeError("Options cannot have nargs < 0")
if self.prompt and self.is_flag and not self.is_bool_flag: if self.prompt and self.is_flag and not self.is_bool_flag:
raise TypeError('Cannot prompt for flags that are not bools.') raise TypeError("Cannot prompt for flags that are not bools.")
if not self.is_bool_flag and self.secondary_opts: if not self.is_bool_flag and self.secondary_opts:
raise TypeError('Got secondary option for non boolean flag.') raise TypeError("Got secondary option for non boolean flag.")
if self.is_bool_flag and self.hide_input \ if self.is_bool_flag and self.hide_input and self.prompt is not None:
and self.prompt is not None: raise TypeError("Hidden input does not work with boolean flag prompts.")
raise TypeError('Hidden input does not work with boolean '
'flag prompts.')
if self.count: if self.count:
if self.multiple: if self.multiple:
raise TypeError('Options cannot be multiple and count ' raise TypeError(
'at the same time.') "Options cannot be multiple and count at the same time."
)
elif self.is_flag: elif self.is_flag:
raise TypeError('Options cannot be count and flags at ' raise TypeError(
'the same time.') "Options cannot be count and flags at the same time."
)
def _parse_decls(self, decls, expose_value): def _parse_decls(self, decls, expose_value):
opts = [] opts = []
@ -1620,10 +1786,10 @@ class Option(Parameter):
for decl in decls: for decl in decls:
if isidentifier(decl): if isidentifier(decl):
if name is not None: if name is not None:
raise TypeError('Name defined twice') raise TypeError("Name defined twice")
name = decl name = decl
else: else:
split_char = decl[:1] == '/' and ';' or '/' split_char = ";" if decl[:1] == "/" else "/"
if split_char in decl: if split_char in decl:
first, second = decl.split(split_char, 1) first, second = decl.split(split_char, 1)
first = first.rstrip() first = first.rstrip()
@ -1639,49 +1805,51 @@ class Option(Parameter):
if name is None and possible_names: if name is None and possible_names:
possible_names.sort(key=lambda x: -len(x[0])) # group long options first possible_names.sort(key=lambda x: -len(x[0])) # group long options first
name = possible_names[0][1].replace('-', '_').lower() name = possible_names[0][1].replace("-", "_").lower()
if not isidentifier(name): if not isidentifier(name):
name = None name = None
if name is None: if name is None:
if not expose_value: if not expose_value:
return None, opts, secondary_opts return None, opts, secondary_opts
raise TypeError('Could not determine name for option') raise TypeError("Could not determine name for option")
if not opts and not secondary_opts: if not opts and not secondary_opts:
raise TypeError('No options defined but a name was passed (%s). ' raise TypeError(
'Did you mean to declare an argument instead ' "No options defined but a name was passed ({}). Did you"
'of an option?' % name) " mean to declare an argument instead of an option?".format(name)
)
return name, opts, secondary_opts return name, opts, secondary_opts
def add_to_parser(self, parser, ctx): def add_to_parser(self, parser, ctx):
kwargs = { kwargs = {
'dest': self.name, "dest": self.name,
'nargs': self.nargs, "nargs": self.nargs,
'obj': self, "obj": self,
} }
if self.multiple: if self.multiple:
action = 'append' action = "append"
elif self.count: elif self.count:
action = 'count' action = "count"
else: else:
action = 'store' action = "store"
if self.is_flag: if self.is_flag:
kwargs.pop('nargs', None) kwargs.pop("nargs", None)
action_const = "{}_const".format(action)
if self.is_bool_flag and self.secondary_opts: if self.is_bool_flag and self.secondary_opts:
parser.add_option(self.opts, action=action + '_const', parser.add_option(self.opts, action=action_const, const=True, **kwargs)
const=True, **kwargs) parser.add_option(
parser.add_option(self.secondary_opts, action=action + self.secondary_opts, action=action_const, const=False, **kwargs
'_const', const=False, **kwargs) )
else: else:
parser.add_option(self.opts, action=action + '_const', parser.add_option(
const=self.flag_value, self.opts, action=action_const, const=self.flag_value, **kwargs
**kwargs) )
else: else:
kwargs['action'] = action kwargs["action"] = action
parser.add_option(self.opts, **kwargs) parser.add_option(self.opts, **kwargs)
def get_help_record(self, ctx): def get_help_record(self, ctx):
@ -1694,46 +1862,50 @@ class Option(Parameter):
if any_slashes: if any_slashes:
any_prefix_is_slash[:] = [True] any_prefix_is_slash[:] = [True]
if not self.is_flag and not self.count: if not self.is_flag and not self.count:
rv += ' ' + self.make_metavar() rv += " {}".format(self.make_metavar())
return rv return rv
rv = [_write_opts(self.opts)] rv = [_write_opts(self.opts)]
if self.secondary_opts: if self.secondary_opts:
rv.append(_write_opts(self.secondary_opts)) rv.append(_write_opts(self.secondary_opts))
help = self.help or '' help = self.help or ""
extra = [] extra = []
if self.show_envvar: if self.show_envvar:
envvar = self.envvar envvar = self.envvar
if envvar is None: if envvar is None:
if self.allow_from_autoenv and \ if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:
ctx.auto_envvar_prefix is not None: envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper())
envvar = '%s_%s' % (ctx.auto_envvar_prefix, self.name.upper())
if envvar is not None: if envvar is not None:
extra.append('env var: %s' % ( extra.append(
', '.join('%s' % d for d in envvar) "env var: {}".format(
if isinstance(envvar, (list, tuple)) ", ".join(str(d) for d in envvar)
else envvar, )) if isinstance(envvar, (list, tuple))
if self.default is not None and self.show_default: else envvar
)
)
if self.default is not None and (self.show_default or ctx.show_default):
if isinstance(self.show_default, string_types): if isinstance(self.show_default, string_types):
default_string = '({})'.format(self.show_default) default_string = "({})".format(self.show_default)
elif isinstance(self.default, (list, tuple)): elif isinstance(self.default, (list, tuple)):
default_string = ', '.join('%s' % d for d in self.default) default_string = ", ".join(str(d) for d in self.default)
elif inspect.isfunction(self.default): elif inspect.isfunction(self.default):
default_string = "(dynamic)" default_string = "(dynamic)"
else: else:
default_string = self.default default_string = self.default
extra.append('default: {}'.format(default_string)) extra.append("default: {}".format(default_string))
if self.required: if self.required:
extra.append('required') extra.append("required")
if extra: if extra:
help = '%s[%s]' % (help and help + ' ' or '', '; '.join(extra)) help = "{}[{}]".format(
"{} ".format(help) if help else "", "; ".join(extra)
)
return ((any_prefix_is_slash and '; ' or ' / ').join(rv), help) return ("; " if any_prefix_is_slash else " / ").join(rv), help
def get_default(self, ctx): def get_default(self, ctx):
# If we're a non boolean flag out default is more complex because # If we're a non boolean flag our default is more complex because
# we need to look at all flags in the same group to figure out # we need to look at all flags in the same group to figure out
# if we're the the default one in which case we return the flag # if we're the the default one in which case we return the flag
# value as default. # value as default.
@ -1758,18 +1930,22 @@ class Option(Parameter):
if self.is_bool_flag: if self.is_bool_flag:
return confirm(self.prompt, default) return confirm(self.prompt, default)
return prompt(self.prompt, default=default, type=self.type, return prompt(
hide_input=self.hide_input, show_choices=self.show_choices, self.prompt,
confirmation_prompt=self.confirmation_prompt, default=default,
value_proc=lambda x: self.process_value(ctx, x)) type=self.type,
hide_input=self.hide_input,
show_choices=self.show_choices,
confirmation_prompt=self.confirmation_prompt,
value_proc=lambda x: self.process_value(ctx, x),
)
def resolve_envvar_value(self, ctx): def resolve_envvar_value(self, ctx):
rv = Parameter.resolve_envvar_value(self, ctx) rv = Parameter.resolve_envvar_value(self, ctx)
if rv is not None: if rv is not None:
return rv return rv
if self.allow_from_autoenv and \ if self.allow_from_autoenv and ctx.auto_envvar_prefix is not None:
ctx.auto_envvar_prefix is not None: envvar = "{}_{}".format(ctx.auto_envvar_prefix, self.name.upper())
envvar = '%s_%s' % (ctx.auto_envvar_prefix, self.name.upper())
return os.environ.get(envvar) return os.environ.get(envvar)
def value_from_envvar(self, ctx): def value_from_envvar(self, ctx):
@ -1784,8 +1960,7 @@ class Option(Parameter):
return rv return rv
def full_process_value(self, ctx, value): def full_process_value(self, ctx, value):
if value is None and self.prompt is not None \ if value is None and self.prompt is not None and not ctx.resilient_parsing:
and not ctx.resilient_parsing:
return self.prompt_for_value(ctx) return self.prompt_for_value(ctx)
return Parameter.full_process_value(self, ctx, value) return Parameter.full_process_value(self, ctx, value)
@ -1797,18 +1972,20 @@ class Argument(Parameter):
All parameters are passed onwards to the parameter constructor. All parameters are passed onwards to the parameter constructor.
""" """
param_type_name = 'argument'
param_type_name = "argument"
def __init__(self, param_decls, required=None, **attrs): def __init__(self, param_decls, required=None, **attrs):
if required is None: if required is None:
if attrs.get('default') is not None: if attrs.get("default") is not None:
required = False required = False
else: else:
required = attrs.get('nargs', 1) > 0 required = attrs.get("nargs", 1) > 0
Parameter.__init__(self, param_decls, required=required, **attrs) Parameter.__init__(self, param_decls, required=required, **attrs)
if self.default is not None and self.nargs < 0: if self.default is not None and self.nargs < 0:
raise TypeError('nargs=-1 in combination with a default value ' raise TypeError(
'is not supported.') "nargs=-1 in combination with a default value is not supported."
)
@property @property
def human_readable_name(self): def human_readable_name(self):
@ -1823,34 +2000,31 @@ class Argument(Parameter):
if not var: if not var:
var = self.name.upper() var = self.name.upper()
if not self.required: if not self.required:
var = '[%s]' % var var = "[{}]".format(var)
if self.nargs != 1: if self.nargs != 1:
var += '...' var += "..."
return var return var
def _parse_decls(self, decls, expose_value): def _parse_decls(self, decls, expose_value):
if not decls: if not decls:
if not expose_value: if not expose_value:
return None, [], [] return None, [], []
raise TypeError('Could not determine name for argument') raise TypeError("Could not determine name for argument")
if len(decls) == 1: if len(decls) == 1:
name = arg = decls[0] name = arg = decls[0]
name = name.replace('-', '_').lower() name = name.replace("-", "_").lower()
else: else:
raise TypeError('Arguments take exactly one ' raise TypeError(
'parameter declaration, got %d' % len(decls)) "Arguments take exactly one parameter declaration, got"
" {}".format(len(decls))
)
return name, [arg], [] return name, [arg], []
def get_usage_pieces(self, ctx): def get_usage_pieces(self, ctx):
return [self.make_metavar()] return [self.make_metavar()]
def get_error_hint(self, ctx): def get_error_hint(self, ctx):
return '"%s"' % self.make_metavar() return repr(self.make_metavar())
def add_to_parser(self, parser, ctx): def add_to_parser(self, parser, ctx):
parser.add_argument(dest=self.name, nargs=self.nargs, parser.add_argument(dest=self.name, nargs=self.nargs, obj=self)
obj=self)
# Circular dependency between decorators and core
from .decorators import command, group

View file

@ -1,20 +1,25 @@
import sys
import inspect import inspect
import sys
from functools import update_wrapper from functools import update_wrapper
from ._compat import iteritems from ._compat import iteritems
from ._unicodefun import _check_for_unicode_literals from ._unicodefun import _check_for_unicode_literals
from .utils import echo from .core import Argument
from .core import Command
from .core import Group
from .core import Option
from .globals import get_current_context from .globals import get_current_context
from .utils import echo
def pass_context(f): def pass_context(f):
"""Marks a callback as wanting to receive the current context """Marks a callback as wanting to receive the current context
object as first argument. object as first argument.
""" """
def new_func(*args, **kwargs): def new_func(*args, **kwargs):
return f(get_current_context(), *args, **kwargs) return f(get_current_context(), *args, **kwargs)
return update_wrapper(new_func, f) return update_wrapper(new_func, f)
@ -23,8 +28,10 @@ def pass_obj(f):
context onwards (:attr:`Context.obj`). This is useful if that object context onwards (:attr:`Context.obj`). This is useful if that object
represents the state of a nested system. represents the state of a nested system.
""" """
def new_func(*args, **kwargs): def new_func(*args, **kwargs):
return f(get_current_context().obj, *args, **kwargs) return f(get_current_context().obj, *args, **kwargs)
return update_wrapper(new_func, f) return update_wrapper(new_func, f)
@ -50,6 +57,7 @@ def make_pass_decorator(object_type, ensure=False):
:param ensure: if set to `True`, a new object will be created and :param ensure: if set to `True`, a new object will be created and
remembered on the context if it's not there yet. remembered on the context if it's not there yet.
""" """
def decorator(f): def decorator(f):
def new_func(*args, **kwargs): def new_func(*args, **kwargs):
ctx = get_current_context() ctx = get_current_context()
@ -58,35 +66,41 @@ def make_pass_decorator(object_type, ensure=False):
else: else:
obj = ctx.find_object(object_type) obj = ctx.find_object(object_type)
if obj is None: if obj is None:
raise RuntimeError('Managed to invoke callback without a ' raise RuntimeError(
'context object of type %r existing' "Managed to invoke callback without a context"
% object_type.__name__) " object of type '{}' existing".format(object_type.__name__)
)
return ctx.invoke(f, obj, *args, **kwargs) return ctx.invoke(f, obj, *args, **kwargs)
return update_wrapper(new_func, f) return update_wrapper(new_func, f)
return decorator return decorator
def _make_command(f, name, attrs, cls): def _make_command(f, name, attrs, cls):
if isinstance(f, Command): if isinstance(f, Command):
raise TypeError('Attempted to convert a callback into a ' raise TypeError("Attempted to convert a callback into a command twice.")
'command twice.')
try: try:
params = f.__click_params__ params = f.__click_params__
params.reverse() params.reverse()
del f.__click_params__ del f.__click_params__
except AttributeError: except AttributeError:
params = [] params = []
help = attrs.get('help') help = attrs.get("help")
if help is None: if help is None:
help = inspect.getdoc(f) help = inspect.getdoc(f)
if isinstance(help, bytes): if isinstance(help, bytes):
help = help.decode('utf-8') help = help.decode("utf-8")
else: else:
help = inspect.cleandoc(help) help = inspect.cleandoc(help)
attrs['help'] = help attrs["help"] = help
_check_for_unicode_literals() _check_for_unicode_literals()
return cls(name=name or f.__name__.lower().replace('_', '-'), return cls(
callback=f, params=params, **attrs) name=name or f.__name__.lower().replace("_", "-"),
callback=f,
params=params,
**attrs
)
def command(name=None, cls=None, **attrs): def command(name=None, cls=None, **attrs):
@ -94,9 +108,9 @@ def command(name=None, cls=None, **attrs):
callback. This will also automatically attach all decorated callback. This will also automatically attach all decorated
:func:`option`\s and :func:`argument`\s as parameters to the command. :func:`option`\s and :func:`argument`\s as parameters to the command.
The name of the command defaults to the name of the function. If you The name of the command defaults to the name of the function with
want to change that, you can pass the intended name as the first underscores replaced by dashes. If you want to change that, you can
argument. pass the intended name as the first argument.
All keyword arguments are forwarded to the underlying command class. All keyword arguments are forwarded to the underlying command class.
@ -111,10 +125,12 @@ def command(name=None, cls=None, **attrs):
""" """
if cls is None: if cls is None:
cls = Command cls = Command
def decorator(f): def decorator(f):
cmd = _make_command(f, name, attrs, cls) cmd = _make_command(f, name, attrs, cls)
cmd.__doc__ = f.__doc__ cmd.__doc__ = f.__doc__
return cmd return cmd
return decorator return decorator
@ -123,7 +139,7 @@ def group(name=None, **attrs):
works otherwise the same as :func:`command` just that the `cls` works otherwise the same as :func:`command` just that the `cls`
parameter is set to :class:`Group`. parameter is set to :class:`Group`.
""" """
attrs.setdefault('cls', Group) attrs.setdefault("cls", Group)
return command(name, **attrs) return command(name, **attrs)
@ -131,7 +147,7 @@ def _param_memo(f, param):
if isinstance(f, Command): if isinstance(f, Command):
f.params.append(param) f.params.append(param)
else: else:
if not hasattr(f, '__click_params__'): if not hasattr(f, "__click_params__"):
f.__click_params__ = [] f.__click_params__ = []
f.__click_params__.append(param) f.__click_params__.append(param)
@ -146,10 +162,12 @@ def argument(*param_decls, **attrs):
:param cls: the argument class to instantiate. This defaults to :param cls: the argument class to instantiate. This defaults to
:class:`Argument`. :class:`Argument`.
""" """
def decorator(f): def decorator(f):
ArgumentClass = attrs.pop('cls', Argument) ArgumentClass = attrs.pop("cls", Argument)
_param_memo(f, ArgumentClass(param_decls, **attrs)) _param_memo(f, ArgumentClass(param_decls, **attrs))
return f return f
return decorator return decorator
@ -163,15 +181,17 @@ def option(*param_decls, **attrs):
:param cls: the option class to instantiate. This defaults to :param cls: the option class to instantiate. This defaults to
:class:`Option`. :class:`Option`.
""" """
def decorator(f): def decorator(f):
# Issue 926, copy attrs, so pre-defined options can re-use the same cls= # Issue 926, copy attrs, so pre-defined options can re-use the same cls=
option_attrs = attrs.copy() option_attrs = attrs.copy()
if 'help' in option_attrs: if "help" in option_attrs:
option_attrs['help'] = inspect.cleandoc(option_attrs['help']) option_attrs["help"] = inspect.cleandoc(option_attrs["help"])
OptionClass = option_attrs.pop('cls', Option) OptionClass = option_attrs.pop("cls", Option)
_param_memo(f, OptionClass(param_decls, **option_attrs)) _param_memo(f, OptionClass(param_decls, **option_attrs))
return f return f
return decorator return decorator
@ -192,16 +212,19 @@ def confirmation_option(*param_decls, **attrs):
def dropdb(): def dropdb():
pass pass
""" """
def decorator(f): def decorator(f):
def callback(ctx, param, value): def callback(ctx, param, value):
if not value: if not value:
ctx.abort() ctx.abort()
attrs.setdefault('is_flag', True)
attrs.setdefault('callback', callback) attrs.setdefault("is_flag", True)
attrs.setdefault('expose_value', False) attrs.setdefault("callback", callback)
attrs.setdefault('prompt', 'Do you want to continue?') attrs.setdefault("expose_value", False)
attrs.setdefault('help', 'Confirm the action without prompting.') attrs.setdefault("prompt", "Do you want to continue?")
return option(*(param_decls or ('--yes',)), **attrs)(f) attrs.setdefault("help", "Confirm the action without prompting.")
return option(*(param_decls or ("--yes",)), **attrs)(f)
return decorator return decorator
@ -217,11 +240,13 @@ def password_option(*param_decls, **attrs):
def changeadmin(password): def changeadmin(password):
pass pass
""" """
def decorator(f): def decorator(f):
attrs.setdefault('prompt', True) attrs.setdefault("prompt", True)
attrs.setdefault('confirmation_prompt', True) attrs.setdefault("confirmation_prompt", True)
attrs.setdefault('hide_input', True) attrs.setdefault("hide_input", True)
return option(*(param_decls or ('--password',)), **attrs)(f) return option(*(param_decls or ("--password",)), **attrs)(f)
return decorator return decorator
@ -238,14 +263,14 @@ def version_option(version=None, *param_decls, **attrs):
:param others: everything else is forwarded to :func:`option`. :param others: everything else is forwarded to :func:`option`.
""" """
if version is None: if version is None:
if hasattr(sys, '_getframe'): if hasattr(sys, "_getframe"):
module = sys._getframe(1).f_globals.get('__name__') module = sys._getframe(1).f_globals.get("__name__")
else: else:
module = '' module = ""
def decorator(f): def decorator(f):
prog_name = attrs.pop('prog_name', None) prog_name = attrs.pop("prog_name", None)
message = attrs.pop('message', '%(prog)s, version %(version)s') message = attrs.pop("message", "%(prog)s, version %(version)s")
def callback(ctx, param, value): def callback(ctx, param, value):
if not value or ctx.resilient_parsing: if not value or ctx.resilient_parsing:
@ -261,25 +286,23 @@ def version_option(version=None, *param_decls, **attrs):
pass pass
else: else:
for dist in pkg_resources.working_set: for dist in pkg_resources.working_set:
scripts = dist.get_entry_map().get('console_scripts') or {} scripts = dist.get_entry_map().get("console_scripts") or {}
for script_name, entry_point in iteritems(scripts): for _, entry_point in iteritems(scripts):
if entry_point.module_name == module: if entry_point.module_name == module:
ver = dist.version ver = dist.version
break break
if ver is None: if ver is None:
raise RuntimeError('Could not determine version') raise RuntimeError("Could not determine version")
echo(message % { echo(message % {"prog": prog, "version": ver}, color=ctx.color)
'prog': prog,
'version': ver,
}, color=ctx.color)
ctx.exit() ctx.exit()
attrs.setdefault('is_flag', True) attrs.setdefault("is_flag", True)
attrs.setdefault('expose_value', False) attrs.setdefault("expose_value", False)
attrs.setdefault('is_eager', True) attrs.setdefault("is_eager", True)
attrs.setdefault('help', 'Show the version and exit.') attrs.setdefault("help", "Show the version and exit.")
attrs['callback'] = callback attrs["callback"] = callback
return option(*(param_decls or ('--version',)), **attrs)(f) return option(*(param_decls or ("--version",)), **attrs)(f)
return decorator return decorator
@ -293,19 +316,18 @@ def help_option(*param_decls, **attrs):
All arguments are forwarded to :func:`option`. All arguments are forwarded to :func:`option`.
""" """
def decorator(f): def decorator(f):
def callback(ctx, param, value): def callback(ctx, param, value):
if value and not ctx.resilient_parsing: if value and not ctx.resilient_parsing:
echo(ctx.get_help(), color=ctx.color) echo(ctx.get_help(), color=ctx.color)
ctx.exit() ctx.exit()
attrs.setdefault('is_flag', True)
attrs.setdefault('expose_value', False) attrs.setdefault("is_flag", True)
attrs.setdefault('help', 'Show this message and exit.') attrs.setdefault("expose_value", False)
attrs.setdefault('is_eager', True) attrs.setdefault("help", "Show this message and exit.")
attrs['callback'] = callback attrs.setdefault("is_eager", True)
return option(*(param_decls or ('--help',)), **attrs)(f) attrs["callback"] = callback
return option(*(param_decls or ("--help",)), **attrs)(f)
return decorator return decorator
# Circular dependencies between core and decorators
from .core import Command, Group, Argument, Option

View file

@ -1,10 +1,12 @@
from ._compat import PY2, filename_to_ui, get_text_stderr from ._compat import filename_to_ui
from ._compat import get_text_stderr
from ._compat import PY2
from .utils import echo from .utils import echo
def _join_param_hints(param_hint): def _join_param_hints(param_hint):
if isinstance(param_hint, (tuple, list)): if isinstance(param_hint, (tuple, list)):
return ' / '.join('"%s"' % x for x in param_hint) return " / ".join(repr(x) for x in param_hint)
return param_hint return param_hint
@ -18,7 +20,7 @@ class ClickException(Exception):
ctor_msg = message ctor_msg = message
if PY2: if PY2:
if ctor_msg is not None: if ctor_msg is not None:
ctor_msg = ctor_msg.encode('utf-8') ctor_msg = ctor_msg.encode("utf-8")
Exception.__init__(self, ctor_msg) Exception.__init__(self, ctor_msg)
self.message = message self.message = message
@ -32,12 +34,12 @@ class ClickException(Exception):
__unicode__ = __str__ __unicode__ = __str__
def __str__(self): def __str__(self):
return self.message.encode('utf-8') return self.message.encode("utf-8")
def show(self, file=None): def show(self, file=None):
if file is None: if file is None:
file = get_text_stderr() file = get_text_stderr()
echo('Error: %s' % self.format_message(), file=file) echo("Error: {}".format(self.format_message()), file=file)
class UsageError(ClickException): class UsageError(ClickException):
@ -48,26 +50,27 @@ class UsageError(ClickException):
:param ctx: optionally the context that caused this error. Click will :param ctx: optionally the context that caused this error. Click will
fill in the context automatically in some situations. fill in the context automatically in some situations.
""" """
exit_code = 2 exit_code = 2
def __init__(self, message, ctx=None): def __init__(self, message, ctx=None):
ClickException.__init__(self, message) ClickException.__init__(self, message)
self.ctx = ctx self.ctx = ctx
self.cmd = self.ctx and self.ctx.command or None self.cmd = self.ctx.command if self.ctx else None
def show(self, file=None): def show(self, file=None):
if file is None: if file is None:
file = get_text_stderr() file = get_text_stderr()
color = None color = None
hint = '' hint = ""
if (self.cmd is not None and if self.cmd is not None and self.cmd.get_help_option(self.ctx) is not None:
self.cmd.get_help_option(self.ctx) is not None): hint = "Try '{} {}' for help.\n".format(
hint = ('Try "%s %s" for help.\n' self.ctx.command_path, self.ctx.help_option_names[0]
% (self.ctx.command_path, self.ctx.help_option_names[0])) )
if self.ctx is not None: if self.ctx is not None:
color = self.ctx.color color = self.ctx.color
echo(self.ctx.get_usage() + '\n%s' % hint, file=file, color=color) echo("{}\n{}".format(self.ctx.get_usage(), hint), file=file, color=color)
echo('Error: %s' % self.format_message(), file=file, color=color) echo("Error: {}".format(self.format_message()), file=file, color=color)
class BadParameter(UsageError): class BadParameter(UsageError):
@ -88,8 +91,7 @@ class BadParameter(UsageError):
each item is quoted and separated. each item is quoted and separated.
""" """
def __init__(self, message, ctx=None, param=None, def __init__(self, message, ctx=None, param=None, param_hint=None):
param_hint=None):
UsageError.__init__(self, message, ctx) UsageError.__init__(self, message, ctx)
self.param = param self.param = param
self.param_hint = param_hint self.param_hint = param_hint
@ -100,10 +102,10 @@ class BadParameter(UsageError):
elif self.param is not None: elif self.param is not None:
param_hint = self.param.get_error_hint(self.ctx) param_hint = self.param.get_error_hint(self.ctx)
else: else:
return 'Invalid value: %s' % self.message return "Invalid value: {}".format(self.message)
param_hint = _join_param_hints(param_hint) param_hint = _join_param_hints(param_hint)
return 'Invalid value for %s: %s' % (param_hint, self.message) return "Invalid value for {}: {}".format(param_hint, self.message)
class MissingParameter(BadParameter): class MissingParameter(BadParameter):
@ -118,8 +120,9 @@ class MissingParameter(BadParameter):
``'option'`` or ``'argument'``. ``'option'`` or ``'argument'``.
""" """
def __init__(self, message=None, ctx=None, param=None, def __init__(
param_hint=None, param_type=None): self, message=None, ctx=None, param=None, param_hint=None, param_type=None
):
BadParameter.__init__(self, message, ctx, param, param_hint) BadParameter.__init__(self, message, ctx, param, param_hint)
self.param_type = param_type self.param_type = param_type
@ -141,17 +144,30 @@ class MissingParameter(BadParameter):
msg_extra = self.param.type.get_missing_message(self.param) msg_extra = self.param.type.get_missing_message(self.param)
if msg_extra: if msg_extra:
if msg: if msg:
msg += '. ' + msg_extra msg += ". {}".format(msg_extra)
else: else:
msg = msg_extra msg = msg_extra
return 'Missing %s%s%s%s' % ( return "Missing {}{}{}{}".format(
param_type, param_type,
param_hint and ' %s' % param_hint or '', " {}".format(param_hint) if param_hint else "",
msg and '. ' or '.', ". " if msg else ".",
msg or '', msg or "",
) )
def __str__(self):
if self.message is None:
param_name = self.param.name if self.param else None
return "missing parameter: {}".format(param_name)
else:
return self.message
if PY2:
__unicode__ = __str__
def __str__(self):
return self.__unicode__().encode("utf-8")
class NoSuchOption(UsageError): class NoSuchOption(UsageError):
"""Raised if click attempted to handle an option that does not """Raised if click attempted to handle an option that does not
@ -160,10 +176,9 @@ class NoSuchOption(UsageError):
.. versionadded:: 4.0 .. versionadded:: 4.0
""" """
def __init__(self, option_name, message=None, possibilities=None, def __init__(self, option_name, message=None, possibilities=None, ctx=None):
ctx=None):
if message is None: if message is None:
message = 'no such option: %s' % option_name message = "no such option: {}".format(option_name)
UsageError.__init__(self, message, ctx) UsageError.__init__(self, message, ctx)
self.option_name = option_name self.option_name = option_name
self.possibilities = possibilities self.possibilities = possibilities
@ -172,11 +187,11 @@ class NoSuchOption(UsageError):
bits = [self.message] bits = [self.message]
if self.possibilities: if self.possibilities:
if len(self.possibilities) == 1: if len(self.possibilities) == 1:
bits.append('Did you mean %s?' % self.possibilities[0]) bits.append("Did you mean {}?".format(self.possibilities[0]))
else: else:
possibilities = sorted(self.possibilities) possibilities = sorted(self.possibilities)
bits.append('(Possible options: %s)' % ', '.join(possibilities)) bits.append("(Possible options: {})".format(", ".join(possibilities)))
return ' '.join(bits) return " ".join(bits)
class BadOptionUsage(UsageError): class BadOptionUsage(UsageError):
@ -212,13 +227,13 @@ class FileError(ClickException):
def __init__(self, filename, hint=None): def __init__(self, filename, hint=None):
ui_filename = filename_to_ui(filename) ui_filename = filename_to_ui(filename)
if hint is None: if hint is None:
hint = 'unknown error' hint = "unknown error"
ClickException.__init__(self, hint) ClickException.__init__(self, hint)
self.ui_filename = ui_filename self.ui_filename = ui_filename
self.filename = filename self.filename = filename
def format_message(self): def format_message(self):
return 'Could not open file %s: %s' % (self.ui_filename, self.message) return "Could not open file {}: {}".format(self.ui_filename, self.message)
class Abort(RuntimeError): class Abort(RuntimeError):
@ -231,5 +246,8 @@ class Exit(RuntimeError):
:param code: the status code to exit with. :param code: the status code to exit with.
""" """
__slots__ = ("exit_code",)
def __init__(self, code=0): def __init__(self, code=0):
self.exit_code = code self.exit_code = code

View file

@ -1,8 +1,8 @@
from contextlib import contextmanager from contextlib import contextmanager
from .termui import get_terminal_size
from .parser import split_opt
from ._compat import term_len
from ._compat import term_len
from .parser import split_opt
from .termui import get_terminal_size
# Can force a width. This is used by the test system # Can force a width. This is used by the test system
FORCED_WIDTH = None FORCED_WIDTH = None
@ -19,11 +19,12 @@ def measure_table(rows):
def iter_rows(rows, col_count): def iter_rows(rows, col_count):
for row in rows: for row in rows:
row = tuple(row) row = tuple(row)
yield row + ('',) * (col_count - len(row)) yield row + ("",) * (col_count - len(row))
def wrap_text(text, width=78, initial_indent='', subsequent_indent='', def wrap_text(
preserve_paragraphs=False): text, width=78, initial_indent="", subsequent_indent="", preserve_paragraphs=False
):
"""A helper function that intelligently wraps text. By default, it """A helper function that intelligently wraps text. By default, it
assumes that it operates on a single paragraph of text but if the assumes that it operates on a single paragraph of text but if the
`preserve_paragraphs` parameter is provided it will intelligently `preserve_paragraphs` parameter is provided it will intelligently
@ -43,10 +44,14 @@ def wrap_text(text, width=78, initial_indent='', subsequent_indent='',
intelligently handle paragraphs. intelligently handle paragraphs.
""" """
from ._textwrap import TextWrapper from ._textwrap import TextWrapper
text = text.expandtabs() text = text.expandtabs()
wrapper = TextWrapper(width, initial_indent=initial_indent, wrapper = TextWrapper(
subsequent_indent=subsequent_indent, width,
replace_whitespace=False) initial_indent=initial_indent,
subsequent_indent=subsequent_indent,
replace_whitespace=False,
)
if not preserve_paragraphs: if not preserve_paragraphs:
return wrapper.fill(text) return wrapper.fill(text)
@ -57,10 +62,10 @@ def wrap_text(text, width=78, initial_indent='', subsequent_indent='',
def _flush_par(): def _flush_par():
if not buf: if not buf:
return return
if buf[0].strip() == '\b': if buf[0].strip() == "\b":
p.append((indent or 0, True, '\n'.join(buf[1:]))) p.append((indent or 0, True, "\n".join(buf[1:])))
else: else:
p.append((indent or 0, False, ' '.join(buf))) p.append((indent or 0, False, " ".join(buf)))
del buf[:] del buf[:]
for line in text.splitlines(): for line in text.splitlines():
@ -77,13 +82,13 @@ def wrap_text(text, width=78, initial_indent='', subsequent_indent='',
rv = [] rv = []
for indent, raw, text in p: for indent, raw, text in p:
with wrapper.extra_indent(' ' * indent): with wrapper.extra_indent(" " * indent):
if raw: if raw:
rv.append(wrapper.indent_only(text)) rv.append(wrapper.indent_only(text))
else: else:
rv.append(wrapper.fill(text)) rv.append(wrapper.fill(text))
return '\n\n'.join(rv) return "\n\n".join(rv)
class HelpFormatter(object): class HelpFormatter(object):
@ -122,53 +127,65 @@ class HelpFormatter(object):
"""Decreases the indentation.""" """Decreases the indentation."""
self.current_indent -= self.indent_increment self.current_indent -= self.indent_increment
def write_usage(self, prog, args='', prefix='Usage: '): def write_usage(self, prog, args="", prefix="Usage: "):
"""Writes a usage line into the buffer. """Writes a usage line into the buffer.
:param prog: the program name. :param prog: the program name.
:param args: whitespace separated list of arguments. :param args: whitespace separated list of arguments.
:param prefix: the prefix for the first line. :param prefix: the prefix for the first line.
""" """
usage_prefix = '%*s%s ' % (self.current_indent, prefix, prog) usage_prefix = "{:>{w}}{} ".format(prefix, prog, w=self.current_indent)
text_width = self.width - self.current_indent text_width = self.width - self.current_indent
if text_width >= (term_len(usage_prefix) + 20): if text_width >= (term_len(usage_prefix) + 20):
# The arguments will fit to the right of the prefix. # The arguments will fit to the right of the prefix.
indent = ' ' * term_len(usage_prefix) indent = " " * term_len(usage_prefix)
self.write(wrap_text(args, text_width, self.write(
initial_indent=usage_prefix, wrap_text(
subsequent_indent=indent)) args,
text_width,
initial_indent=usage_prefix,
subsequent_indent=indent,
)
)
else: else:
# The prefix is too long, put the arguments on the next line. # The prefix is too long, put the arguments on the next line.
self.write(usage_prefix) self.write(usage_prefix)
self.write('\n') self.write("\n")
indent = ' ' * (max(self.current_indent, term_len(prefix)) + 4) indent = " " * (max(self.current_indent, term_len(prefix)) + 4)
self.write(wrap_text(args, text_width, self.write(
initial_indent=indent, wrap_text(
subsequent_indent=indent)) args, text_width, initial_indent=indent, subsequent_indent=indent
)
)
self.write('\n') self.write("\n")
def write_heading(self, heading): def write_heading(self, heading):
"""Writes a heading into the buffer.""" """Writes a heading into the buffer."""
self.write('%*s%s:\n' % (self.current_indent, '', heading)) self.write("{:>{w}}{}:\n".format("", heading, w=self.current_indent))
def write_paragraph(self): def write_paragraph(self):
"""Writes a paragraph into the buffer.""" """Writes a paragraph into the buffer."""
if self.buffer: if self.buffer:
self.write('\n') self.write("\n")
def write_text(self, text): def write_text(self, text):
"""Writes re-indented text into the buffer. This rewraps and """Writes re-indented text into the buffer. This rewraps and
preserves paragraphs. preserves paragraphs.
""" """
text_width = max(self.width - self.current_indent, 11) text_width = max(self.width - self.current_indent, 11)
indent = ' ' * self.current_indent indent = " " * self.current_indent
self.write(wrap_text(text, text_width, self.write(
initial_indent=indent, wrap_text(
subsequent_indent=indent, text,
preserve_paragraphs=True)) text_width,
self.write('\n') initial_indent=indent,
subsequent_indent=indent,
preserve_paragraphs=True,
)
)
self.write("\n")
def write_dl(self, rows, col_max=30, col_spacing=2): def write_dl(self, rows, col_max=30, col_spacing=2):
"""Writes a definition list into the buffer. This is how options """Writes a definition list into the buffer. This is how options
@ -182,30 +199,40 @@ class HelpFormatter(object):
rows = list(rows) rows = list(rows)
widths = measure_table(rows) widths = measure_table(rows)
if len(widths) != 2: if len(widths) != 2:
raise TypeError('Expected two columns for definition list') raise TypeError("Expected two columns for definition list")
first_col = min(widths[0], col_max) + col_spacing first_col = min(widths[0], col_max) + col_spacing
for first, second in iter_rows(rows, len(widths)): for first, second in iter_rows(rows, len(widths)):
self.write('%*s%s' % (self.current_indent, '', first)) self.write("{:>{w}}{}".format("", first, w=self.current_indent))
if not second: if not second:
self.write('\n') self.write("\n")
continue continue
if term_len(first) <= first_col - col_spacing: if term_len(first) <= first_col - col_spacing:
self.write(' ' * (first_col - term_len(first))) self.write(" " * (first_col - term_len(first)))
else: else:
self.write('\n') self.write("\n")
self.write(' ' * (first_col + self.current_indent)) self.write(" " * (first_col + self.current_indent))
text_width = max(self.width - first_col - 2, 10) text_width = max(self.width - first_col - 2, 10)
lines = iter(wrap_text(second, text_width).splitlines()) wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True)
lines = wrapped_text.splitlines()
if lines: if lines:
self.write(next(lines) + '\n') self.write("{}\n".format(lines[0]))
for line in lines:
self.write('%*s%s\n' % ( for line in lines[1:]:
first_col + self.current_indent, '', line)) self.write(
"{:>{w}}{}\n".format(
"", line, w=first_col + self.current_indent
)
)
if len(lines) > 1:
# separate long help from next option
self.write("\n")
else: else:
self.write('\n') self.write("\n")
@contextmanager @contextmanager
def section(self, name): def section(self, name):
@ -233,7 +260,7 @@ class HelpFormatter(object):
def getvalue(self): def getvalue(self):
"""Returns the buffer contents.""" """Returns the buffer contents."""
return ''.join(self.buffer) return "".join(self.buffer)
def join_options(options): def join_options(options):
@ -246,11 +273,11 @@ def join_options(options):
any_prefix_is_slash = False any_prefix_is_slash = False
for opt in options: for opt in options:
prefix = split_opt(opt)[0] prefix = split_opt(opt)[0]
if prefix == '/': if prefix == "/":
any_prefix_is_slash = True any_prefix_is_slash = True
rv.append((len(prefix), opt)) rv.append((len(prefix), opt))
rv.sort(key=lambda x: x[0]) rv.sort(key=lambda x: x[0])
rv = ', '.join(x[1] for x in rv) rv = ", ".join(x[1] for x in rv)
return rv, any_prefix_is_slash return rv, any_prefix_is_slash

View file

@ -1,6 +1,5 @@
from threading import local from threading import local
_local = local() _local = local()
@ -15,20 +14,20 @@ def get_current_context(silent=False):
.. versionadded:: 5.0 .. versionadded:: 5.0
:param silent: is set to `True` the return value is `None` if no context :param silent: if set to `True` the return value is `None` if no context
is available. The default behavior is to raise a is available. The default behavior is to raise a
:exc:`RuntimeError`. :exc:`RuntimeError`.
""" """
try: try:
return getattr(_local, 'stack')[-1] return _local.stack[-1]
except (AttributeError, IndexError): except (AttributeError, IndexError):
if not silent: if not silent:
raise RuntimeError('There is no active click context.') raise RuntimeError("There is no active click context.")
def push_context(ctx): def push_context(ctx):
"""Pushes a new context to the current stack.""" """Pushes a new context to the current stack."""
_local.__dict__.setdefault('stack', []).append(ctx) _local.__dict__.setdefault("stack", []).append(ctx)
def pop_context(): def pop_context():

View file

@ -1,8 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
click.parser
~~~~~~~~~~~~
This module started out as largely a copy paste from the stdlib's This module started out as largely a copy paste from the stdlib's
optparse module with the features removed that we do not need from optparse module with the features removed that we do not need from
optparse because we implement them in Click on a higher level (for optparse because we implement them in Click on a higher level (for
@ -14,12 +11,20 @@ The reason this is a different module and not optparse from the stdlib
is that there are differences in 2.x and 3.x about the error messages is that there are differences in 2.x and 3.x about the error messages
generated and optparse in the stdlib uses gettext for no good reason generated and optparse in the stdlib uses gettext for no good reason
and might cause us issues. and might cause us issues.
"""
Click uses parts of optparse written by Gregory P. Ward and maintained
by the Python Software Foundation. This is limited to code in parser.py.
Copyright 2001-2006 Gregory P. Ward. All rights reserved.
Copyright 2002-2006 Python Software Foundation. All rights reserved.
"""
import re import re
from collections import deque from collections import deque
from .exceptions import UsageError, NoSuchOption, BadOptionUsage, \
BadArgumentUsage from .exceptions import BadArgumentUsage
from .exceptions import BadOptionUsage
from .exceptions import NoSuchOption
from .exceptions import UsageError
def _unpack_args(args, nargs_spec): def _unpack_args(args, nargs_spec):
@ -59,7 +64,7 @@ def _unpack_args(args, nargs_spec):
rv.append(tuple(x)) rv.append(tuple(x))
elif nargs < 0: elif nargs < 0:
if spos is not None: if spos is not None:
raise TypeError('Cannot have two nargs < 0') raise TypeError("Cannot have two nargs < 0")
spos = len(rv) spos = len(rv)
rv.append(None) rv.append(None)
@ -68,21 +73,21 @@ def _unpack_args(args, nargs_spec):
if spos is not None: if spos is not None:
rv[spos] = tuple(args) rv[spos] = tuple(args)
args = [] args = []
rv[spos + 1:] = reversed(rv[spos + 1:]) rv[spos + 1 :] = reversed(rv[spos + 1 :])
return tuple(rv), list(args) return tuple(rv), list(args)
def _error_opt_args(nargs, opt): def _error_opt_args(nargs, opt):
if nargs == 1: if nargs == 1:
raise BadOptionUsage(opt, '%s option requires an argument' % opt) raise BadOptionUsage(opt, "{} option requires an argument".format(opt))
raise BadOptionUsage(opt, '%s option requires %d arguments' % (opt, nargs)) raise BadOptionUsage(opt, "{} option requires {} arguments".format(opt, nargs))
def split_opt(opt): def split_opt(opt):
first = opt[:1] first = opt[:1]
if first.isalnum(): if first.isalnum():
return '', opt return "", opt
if opt[1:2] == first: if opt[1:2] == first:
return opt[:2], opt[2:] return opt[:2], opt[2:]
return first, opt[1:] return first, opt[1:]
@ -98,13 +103,14 @@ def normalize_opt(opt, ctx):
def split_arg_string(string): def split_arg_string(string):
"""Given an argument string this attempts to split it into small parts.""" """Given an argument string this attempts to split it into small parts."""
rv = [] rv = []
for match in re.finditer(r"('([^'\\]*(?:\\.[^'\\]*)*)'" for match in re.finditer(
r'|"([^"\\]*(?:\\.[^"\\]*)*)"' r"('([^'\\]*(?:\\.[^'\\]*)*)'|\"([^\"\\]*(?:\\.[^\"\\]*)*)\"|\S+)\s*",
r'|\S+)\s*', string, re.S): string,
re.S,
):
arg = match.group().strip() arg = match.group().strip()
if arg[:1] == arg[-1:] and arg[:1] in '"\'': if arg[:1] == arg[-1:] and arg[:1] in "\"'":
arg = arg[1:-1].encode('ascii', 'backslashreplace') \ arg = arg[1:-1].encode("ascii", "backslashreplace").decode("unicode-escape")
.decode('unicode-escape')
try: try:
arg = type(string)(arg) arg = type(string)(arg)
except UnicodeError: except UnicodeError:
@ -114,7 +120,6 @@ def split_arg_string(string):
class Option(object): class Option(object):
def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None): def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None):
self._short_opts = [] self._short_opts = []
self._long_opts = [] self._long_opts = []
@ -123,8 +128,7 @@ class Option(object):
for opt in opts: for opt in opts:
prefix, value = split_opt(opt) prefix, value = split_opt(opt)
if not prefix: if not prefix:
raise ValueError('Invalid start character for option (%s)' raise ValueError("Invalid start character for option ({})".format(opt))
% opt)
self.prefixes.add(prefix[0]) self.prefixes.add(prefix[0])
if len(prefix) == 1 and len(value) == 1: if len(prefix) == 1 and len(value) == 1:
self._short_opts.append(opt) self._short_opts.append(opt)
@ -133,7 +137,7 @@ class Option(object):
self.prefixes.add(prefix) self.prefixes.add(prefix)
if action is None: if action is None:
action = 'store' action = "store"
self.dest = dest self.dest = dest
self.action = action self.action = action
@ -143,26 +147,25 @@ class Option(object):
@property @property
def takes_value(self): def takes_value(self):
return self.action in ('store', 'append') return self.action in ("store", "append")
def process(self, value, state): def process(self, value, state):
if self.action == 'store': if self.action == "store":
state.opts[self.dest] = value state.opts[self.dest] = value
elif self.action == 'store_const': elif self.action == "store_const":
state.opts[self.dest] = self.const state.opts[self.dest] = self.const
elif self.action == 'append': elif self.action == "append":
state.opts.setdefault(self.dest, []).append(value) state.opts.setdefault(self.dest, []).append(value)
elif self.action == 'append_const': elif self.action == "append_const":
state.opts.setdefault(self.dest, []).append(self.const) state.opts.setdefault(self.dest, []).append(self.const)
elif self.action == 'count': elif self.action == "count":
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 state.opts[self.dest] = state.opts.get(self.dest, 0) + 1
else: else:
raise ValueError('unknown action %r' % self.action) raise ValueError("unknown action '{}'".format(self.action))
state.order.append(self.obj) state.order.append(self.obj)
class Argument(object): class Argument(object):
def __init__(self, dest, nargs=1, obj=None): def __init__(self, dest, nargs=1, obj=None):
self.dest = dest self.dest = dest
self.nargs = nargs self.nargs = nargs
@ -174,14 +177,14 @@ class Argument(object):
if holes == len(value): if holes == len(value):
value = None value = None
elif holes != 0: elif holes != 0:
raise BadArgumentUsage('argument %s takes %d values' raise BadArgumentUsage(
% (self.dest, self.nargs)) "argument {} takes {} values".format(self.dest, self.nargs)
)
state.opts[self.dest] = value state.opts[self.dest] = value
state.order.append(self.obj) state.order.append(self.obj)
class ParsingState(object): class ParsingState(object):
def __init__(self, rargs): def __init__(self, rargs):
self.opts = {} self.opts = {}
self.largs = [] self.largs = []
@ -222,11 +225,10 @@ class OptionParser(object):
self.ignore_unknown_options = ctx.ignore_unknown_options self.ignore_unknown_options = ctx.ignore_unknown_options
self._short_opt = {} self._short_opt = {}
self._long_opt = {} self._long_opt = {}
self._opt_prefixes = set(['-', '--']) self._opt_prefixes = {"-", "--"}
self._args = [] self._args = []
def add_option(self, opts, dest, action=None, nargs=1, const=None, def add_option(self, opts, dest, action=None, nargs=1, const=None, obj=None):
obj=None):
"""Adds a new option named `dest` to the parser. The destination """Adds a new option named `dest` to the parser. The destination
is not inferred (unlike with optparse) and needs to be explicitly is not inferred (unlike with optparse) and needs to be explicitly
provided. Action can be any of ``store``, ``store_const``, provided. Action can be any of ``store``, ``store_const``,
@ -238,8 +240,7 @@ class OptionParser(object):
if obj is None: if obj is None:
obj = dest obj = dest
opts = [normalize_opt(opt, self.ctx) for opt in opts] opts = [normalize_opt(opt, self.ctx) for opt in opts]
option = Option(opts, dest, action=action, nargs=nargs, option = Option(opts, dest, action=action, nargs=nargs, const=const, obj=obj)
const=const, obj=obj)
self._opt_prefixes.update(option.prefixes) self._opt_prefixes.update(option.prefixes)
for opt in option._short_opts: for opt in option._short_opts:
self._short_opt[opt] = option self._short_opt[opt] = option
@ -273,8 +274,9 @@ class OptionParser(object):
return state.opts, state.largs, state.order return state.opts, state.largs, state.order
def _process_args_for_args(self, state): def _process_args_for_args(self, state):
pargs, args = _unpack_args(state.largs + state.rargs, pargs, args = _unpack_args(
[x.nargs for x in self._args]) state.largs + state.rargs, [x.nargs for x in self._args]
)
for idx, arg in enumerate(self._args): for idx, arg in enumerate(self._args):
arg.process(pargs[idx], state) arg.process(pargs[idx], state)
@ -288,7 +290,7 @@ class OptionParser(object):
arglen = len(arg) arglen = len(arg)
# Double dashes always handled explicitly regardless of what # Double dashes always handled explicitly regardless of what
# prefixes are valid. # prefixes are valid.
if arg == '--': if arg == "--":
return return
elif arg[:1] in self._opt_prefixes and arglen > 1: elif arg[:1] in self._opt_prefixes and arglen > 1:
self._process_opts(arg, state) self._process_opts(arg, state)
@ -320,8 +322,7 @@ class OptionParser(object):
def _match_long_opt(self, opt, explicit_value, state): def _match_long_opt(self, opt, explicit_value, state):
if opt not in self._long_opt: if opt not in self._long_opt:
possibilities = [word for word in self._long_opt possibilities = [word for word in self._long_opt if word.startswith(opt)]
if word.startswith(opt)]
raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
option = self._long_opt[opt] option = self._long_opt[opt]
@ -343,7 +344,7 @@ class OptionParser(object):
del state.rargs[:nargs] del state.rargs[:nargs]
elif explicit_value is not None: elif explicit_value is not None:
raise BadOptionUsage(opt, '%s option does not take a value' % opt) raise BadOptionUsage(opt, "{} option does not take a value".format(opt))
else: else:
value = None value = None
@ -395,15 +396,15 @@ class OptionParser(object):
# to the state as new larg. This way there is basic combinatorics # to the state as new larg. This way there is basic combinatorics
# that can be achieved while still ignoring unknown arguments. # that can be achieved while still ignoring unknown arguments.
if self.ignore_unknown_options and unknown_options: if self.ignore_unknown_options and unknown_options:
state.largs.append(prefix + ''.join(unknown_options)) state.largs.append("{}{}".format(prefix, "".join(unknown_options)))
def _process_opts(self, arg, state): def _process_opts(self, arg, state):
explicit_value = None explicit_value = None
# Long option handling happens in two parts. The first part is # Long option handling happens in two parts. The first part is
# supporting explicitly attached values. In any case, we will try # supporting explicitly attached values. In any case, we will try
# to long match the option first. # to long match the option first.
if '=' in arg: if "=" in arg:
long_opt, explicit_value = arg.split('=', 1) long_opt, explicit_value = arg.split("=", 1)
else: else:
long_opt = arg long_opt = arg
norm_long_opt = normalize_opt(long_opt, self.ctx) norm_long_opt = normalize_opt(long_opt, self.ctx)

View file

@ -1,60 +1,89 @@
import os
import sys
import struct
import inspect import inspect
import io
import itertools import itertools
import os
import struct
import sys
from ._compat import raw_input, text_type, string_types, \ from ._compat import DEFAULT_COLUMNS
isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN from ._compat import get_winterm_size
from .utils import echo from ._compat import isatty
from .exceptions import Abort, UsageError from ._compat import raw_input
from .types import convert_type, Choice, Path from ._compat import string_types
from ._compat import strip_ansi
from ._compat import text_type
from ._compat import WIN
from .exceptions import Abort
from .exceptions import UsageError
from .globals import resolve_color_default from .globals import resolve_color_default
from .types import Choice
from .types import convert_type
from .types import Path
from .utils import echo
from .utils import LazyFile
# The prompt functions to use. The doc tools currently override these # The prompt functions to use. The doc tools currently override these
# functions to customize how they work. # functions to customize how they work.
visible_prompt_func = raw_input visible_prompt_func = raw_input
_ansi_colors = { _ansi_colors = {
'black': 30, "black": 30,
'red': 31, "red": 31,
'green': 32, "green": 32,
'yellow': 33, "yellow": 33,
'blue': 34, "blue": 34,
'magenta': 35, "magenta": 35,
'cyan': 36, "cyan": 36,
'white': 37, "white": 37,
'reset': 39, "reset": 39,
'bright_black': 90, "bright_black": 90,
'bright_red': 91, "bright_red": 91,
'bright_green': 92, "bright_green": 92,
'bright_yellow': 93, "bright_yellow": 93,
'bright_blue': 94, "bright_blue": 94,
'bright_magenta': 95, "bright_magenta": 95,
'bright_cyan': 96, "bright_cyan": 96,
'bright_white': 97, "bright_white": 97,
} }
_ansi_reset_all = '\033[0m' _ansi_reset_all = "\033[0m"
def hidden_prompt_func(prompt): def hidden_prompt_func(prompt):
import getpass import getpass
return getpass.getpass(prompt) return getpass.getpass(prompt)
def _build_prompt(text, suffix, show_default=False, default=None, show_choices=True, type=None): def _build_prompt(
text, suffix, show_default=False, default=None, show_choices=True, type=None
):
prompt = text prompt = text
if type is not None and show_choices and isinstance(type, Choice): if type is not None and show_choices and isinstance(type, Choice):
prompt += ' (' + ", ".join(map(str, type.choices)) + ')' prompt += " ({})".format(", ".join(map(str, type.choices)))
if default is not None and show_default: if default is not None and show_default:
prompt = '%s [%s]' % (prompt, default) prompt = "{} [{}]".format(prompt, _format_default(default))
return prompt + suffix return prompt + suffix
def prompt(text, default=None, hide_input=False, confirmation_prompt=False, def _format_default(default):
type=None, value_proc=None, prompt_suffix=': ', show_default=True, if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"):
err=False, show_choices=True): return default.name
return default
def prompt(
text,
default=None,
hide_input=False,
confirmation_prompt=False,
type=None,
value_proc=None,
prompt_suffix=": ",
show_default=True,
err=False,
show_choices=True,
):
"""Prompts a user for input. This is a convenience function that can """Prompts a user for input. This is a convenience function that can
be used to prompt a user for input later. be used to prompt a user for input later.
@ -92,12 +121,12 @@ def prompt(text, default=None, hide_input=False, confirmation_prompt=False,
result = None result = None
def prompt_func(text): def prompt_func(text):
f = hide_input and hidden_prompt_func or visible_prompt_func f = hidden_prompt_func if hide_input else visible_prompt_func
try: try:
# Write the prompt separately so that we get nice # Write the prompt separately so that we get nice
# coloring through colorama on Windows # coloring through colorama on Windows
echo(text, nl=False, err=err) echo(text, nl=False, err=err)
return f('') return f("")
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
# getpass doesn't print a newline if the user aborts input with ^C. # getpass doesn't print a newline if the user aborts input with ^C.
# Allegedly this behavior is inherited from getpass(3). # Allegedly this behavior is inherited from getpass(3).
@ -109,7 +138,9 @@ def prompt(text, default=None, hide_input=False, confirmation_prompt=False,
if value_proc is None: if value_proc is None:
value_proc = convert_type(type, default) value_proc = convert_type(type, default)
prompt = _build_prompt(text, prompt_suffix, show_default, default, show_choices, type) prompt = _build_prompt(
text, prompt_suffix, show_default, default, show_choices, type
)
while 1: while 1:
while 1: while 1:
@ -125,21 +156,22 @@ def prompt(text, default=None, hide_input=False, confirmation_prompt=False,
try: try:
result = value_proc(value) result = value_proc(value)
except UsageError as e: except UsageError as e:
echo('Error: %s' % e.message, err=err) echo("Error: {}".format(e.message), err=err) # noqa: B306
continue continue
if not confirmation_prompt: if not confirmation_prompt:
return result return result
while 1: while 1:
value2 = prompt_func('Repeat for confirmation: ') value2 = prompt_func("Repeat for confirmation: ")
if value2: if value2:
break break
if value == value2: if value == value2:
return result return result
echo('Error: the two entered values do not match', err=err) echo("Error: the two entered values do not match", err=err)
def confirm(text, default=False, abort=False, prompt_suffix=': ', def confirm(
show_default=True, err=False): text, default=False, abort=False, prompt_suffix=": ", show_default=True, err=False
):
"""Prompts for confirmation (yes/no question). """Prompts for confirmation (yes/no question).
If the user aborts the input by sending a interrupt signal this If the user aborts the input by sending a interrupt signal this
@ -157,24 +189,25 @@ def confirm(text, default=False, abort=False, prompt_suffix=': ',
:param err: if set to true the file defaults to ``stderr`` instead of :param err: if set to true the file defaults to ``stderr`` instead of
``stdout``, the same as with echo. ``stdout``, the same as with echo.
""" """
prompt = _build_prompt(text, prompt_suffix, show_default, prompt = _build_prompt(
default and 'Y/n' or 'y/N') text, prompt_suffix, show_default, "Y/n" if default else "y/N"
)
while 1: while 1:
try: try:
# Write the prompt separately so that we get nice # Write the prompt separately so that we get nice
# coloring through colorama on Windows # coloring through colorama on Windows
echo(prompt, nl=False, err=err) echo(prompt, nl=False, err=err)
value = visible_prompt_func('').lower().strip() value = visible_prompt_func("").lower().strip()
except (KeyboardInterrupt, EOFError): except (KeyboardInterrupt, EOFError):
raise Abort() raise Abort()
if value in ('y', 'yes'): if value in ("y", "yes"):
rv = True rv = True
elif value in ('n', 'no'): elif value in ("n", "no"):
rv = False rv = False
elif value == '': elif value == "":
rv = default rv = default
else: else:
echo('Error: invalid input', err=err) echo("Error: invalid input", err=err)
continue continue
break break
if abort and not rv: if abort and not rv:
@ -189,7 +222,8 @@ def get_terminal_size():
# If shutil has get_terminal_size() (Python 3.3 and later) use that # If shutil has get_terminal_size() (Python 3.3 and later) use that
if sys.version_info >= (3, 3): if sys.version_info >= (3, 3):
import shutil import shutil
shutil_get_terminal_size = getattr(shutil, 'get_terminal_size', None)
shutil_get_terminal_size = getattr(shutil, "get_terminal_size", None)
if shutil_get_terminal_size: if shutil_get_terminal_size:
sz = shutil_get_terminal_size() sz = shutil_get_terminal_size()
return sz.columns, sz.lines return sz.columns, sz.lines
@ -207,8 +241,8 @@ def get_terminal_size():
try: try:
import fcntl import fcntl
import termios import termios
cr = struct.unpack(
'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
except Exception: except Exception:
return return
return cr return cr
@ -224,8 +258,7 @@ def get_terminal_size():
except Exception: except Exception:
pass pass
if not cr or not cr[0] or not cr[1]: if not cr or not cr[0] or not cr[1]:
cr = (os.environ.get('LINES', 25), cr = (os.environ.get("LINES", 25), os.environ.get("COLUMNS", DEFAULT_COLUMNS))
os.environ.get('COLUMNS', DEFAULT_COLUMNS))
return int(cr[1]), int(cr[0]) return int(cr[1]), int(cr[0])
@ -251,18 +284,29 @@ def echo_via_pager(text_or_generator, color=None):
i = iter(text_or_generator) i = iter(text_or_generator)
# convert every element of i to a text type if necessary # convert every element of i to a text type if necessary
text_generator = (el if isinstance(el, string_types) else text_type(el) text_generator = (el if isinstance(el, string_types) else text_type(el) for el in i)
for el in i)
from ._termui_impl import pager from ._termui_impl import pager
return pager(itertools.chain(text_generator, "\n"), color) return pager(itertools.chain(text_generator, "\n"), color)
def progressbar(iterable=None, length=None, label=None, show_eta=True, def progressbar(
show_percent=None, show_pos=False, iterable=None,
item_show_func=None, fill_char='#', empty_char='-', length=None,
bar_template='%(label)s [%(bar)s] %(info)s', label=None,
info_sep=' ', width=36, file=None, color=None): show_eta=True,
show_percent=None,
show_pos=False,
item_show_func=None,
fill_char="#",
empty_char="-",
bar_template="%(label)s [%(bar)s] %(info)s",
info_sep=" ",
width=36,
file=None,
color=None,
):
"""This function creates an iterable context manager that can be used """This function creates an iterable context manager that can be used
to iterate over something while showing a progress bar. It will to iterate over something while showing a progress bar. It will
either iterate over the `iterable` or `length` items (that are counted either iterate over the `iterable` or `length` items (that are counted
@ -272,11 +316,17 @@ def progressbar(iterable=None, length=None, label=None, show_eta=True,
will not be rendered if the file is not a terminal. will not be rendered if the file is not a terminal.
The context manager creates the progress bar. When the context The context manager creates the progress bar. When the context
manager is entered the progress bar is already displayed. With every manager is entered the progress bar is already created. With every
iteration over the progress bar, the iterable passed to the bar is iteration over the progress bar, the iterable passed to the bar is
advanced and the bar is updated. When the context manager exits, advanced and the bar is updated. When the context manager exits,
a newline is printed and the progress bar is finalized on screen. a newline is printed and the progress bar is finalized on screen.
Note: The progress bar is currently designed for use cases where the
total progress can be expected to take at least several seconds.
Because of this, the ProgressBar class object won't display
progress that is considered too fast, and progress where the time
between steps is less than a second.
No printing must happen or the progress bar will be unintentionally No printing must happen or the progress bar will be unintentionally
destroyed. destroyed.
@ -342,13 +392,24 @@ def progressbar(iterable=None, length=None, label=None, show_eta=True,
which is not the case by default. which is not the case by default.
""" """
from ._termui_impl import ProgressBar from ._termui_impl import ProgressBar
color = resolve_color_default(color) color = resolve_color_default(color)
return ProgressBar(iterable=iterable, length=length, show_eta=show_eta, return ProgressBar(
show_percent=show_percent, show_pos=show_pos, iterable=iterable,
item_show_func=item_show_func, fill_char=fill_char, length=length,
empty_char=empty_char, bar_template=bar_template, show_eta=show_eta,
info_sep=info_sep, file=file, label=label, show_percent=show_percent,
width=width, color=color) show_pos=show_pos,
item_show_func=item_show_func,
fill_char=fill_char,
empty_char=empty_char,
bar_template=bar_template,
info_sep=info_sep,
file=file,
label=label,
width=width,
color=color,
)
def clear(): def clear():
@ -364,13 +425,22 @@ def clear():
# clear the screen by shelling out. Otherwise we can use an escape # clear the screen by shelling out. Otherwise we can use an escape
# sequence. # sequence.
if WIN: if WIN:
os.system('cls') os.system("cls")
else: else:
sys.stdout.write('\033[2J\033[1;1H') sys.stdout.write("\033[2J\033[1;1H")
def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, def style(
blink=None, reverse=None, reset=True): text,
fg=None,
bg=None,
bold=None,
dim=None,
underline=None,
blink=None,
reverse=None,
reset=True,
):
"""Styles a text with ANSI styles and returns the new string. By """Styles a text with ANSI styles and returns the new string. By
default the styling is self contained which means that at the end default the styling is self contained which means that at the end
of the string a reset code is issued. This can be prevented by of the string a reset code is issued. This can be prevented by
@ -425,28 +495,28 @@ def style(text, fg=None, bg=None, bold=None, dim=None, underline=None,
bits = [] bits = []
if fg: if fg:
try: try:
bits.append('\033[%dm' % (_ansi_colors[fg])) bits.append("\033[{}m".format(_ansi_colors[fg]))
except KeyError: except KeyError:
raise TypeError('Unknown color %r' % fg) raise TypeError("Unknown color '{}'".format(fg))
if bg: if bg:
try: try:
bits.append('\033[%dm' % (_ansi_colors[bg] + 10)) bits.append("\033[{}m".format(_ansi_colors[bg] + 10))
except KeyError: except KeyError:
raise TypeError('Unknown color %r' % bg) raise TypeError("Unknown color '{}'".format(bg))
if bold is not None: if bold is not None:
bits.append('\033[%dm' % (1 if bold else 22)) bits.append("\033[{}m".format(1 if bold else 22))
if dim is not None: if dim is not None:
bits.append('\033[%dm' % (2 if dim else 22)) bits.append("\033[{}m".format(2 if dim else 22))
if underline is not None: if underline is not None:
bits.append('\033[%dm' % (4 if underline else 24)) bits.append("\033[{}m".format(4 if underline else 24))
if blink is not None: if blink is not None:
bits.append('\033[%dm' % (5 if blink else 25)) bits.append("\033[{}m".format(5 if blink else 25))
if reverse is not None: if reverse is not None:
bits.append('\033[%dm' % (7 if reverse else 27)) bits.append("\033[{}m".format(7 if reverse else 27))
bits.append(text) bits.append(text)
if reset: if reset:
bits.append(_ansi_reset_all) bits.append(_ansi_reset_all)
return ''.join(bits) return "".join(bits)
def unstyle(text): def unstyle(text):
@ -478,8 +548,9 @@ def secho(message=None, file=None, nl=True, err=False, color=None, **styles):
return echo(message, file=file, nl=nl, err=err, color=color) return echo(message, file=file, nl=nl, err=err, color=color)
def edit(text=None, editor=None, env=None, require_save=True, def edit(
extension='.txt', filename=None): text=None, editor=None, env=None, require_save=True, extension=".txt", filename=None
):
r"""Edits the given text in the defined editor. If an editor is given r"""Edits the given text in the defined editor. If an editor is given
(should be the full path to the executable but the regular operating (should be the full path to the executable but the regular operating
system search path is used for finding the executable) it overrides system search path is used for finding the executable) it overrides
@ -508,8 +579,10 @@ def edit(text=None, editor=None, env=None, require_save=True,
file as an indirection in that case. file as an indirection in that case.
""" """
from ._termui_impl import Editor from ._termui_impl import Editor
editor = Editor(editor=editor, env=env, require_save=require_save,
extension=extension) editor = Editor(
editor=editor, env=env, require_save=require_save, extension=extension
)
if filename is None: if filename is None:
return editor.edit(text) return editor.edit(text)
editor.edit_file(filename) editor.edit_file(filename)
@ -538,6 +611,7 @@ def launch(url, wait=False, locate=False):
the filesystem. the filesystem.
""" """
from ._termui_impl import open_url from ._termui_impl import open_url
return open_url(url, wait=wait, locate=locate) return open_url(url, wait=wait, locate=locate)
@ -574,10 +648,11 @@ def getchar(echo=False):
def raw_terminal(): def raw_terminal():
from ._termui_impl import raw_terminal as f from ._termui_impl import raw_terminal as f
return f() return f()
def pause(info='Press any key to continue ...', err=False): def pause(info="Press any key to continue ...", err=False):
"""This command stops execution and waits for the user to press any """This command stops execution and waits for the user to press any
key to continue. This is similar to the Windows batch "pause" key to continue. This is similar to the Windows batch "pause"
command. If the program is not run through a terminal, this command command. If the program is not run through a terminal, this command

View file

@ -1,18 +1,16 @@
import os
import sys
import shutil
import tempfile
import contextlib import contextlib
import os
import shlex import shlex
import shutil
import sys
import tempfile
from ._compat import iteritems, PY2, string_types from . import formatting
from . import termui
from . import utils
# If someone wants to vendor click, we want to ensure the from ._compat import iteritems
# correct package is discovered. Ideally we could use a from ._compat import PY2
# relative import here but unfortunately Python does not from ._compat import string_types
# support that.
clickpkg = sys.modules[__name__.rsplit('.', 1)[0]]
if PY2: if PY2:
@ -23,7 +21,6 @@ else:
class EchoingStdin(object): class EchoingStdin(object):
def __init__(self, input, output): def __init__(self, input, output):
self._input = input self._input = input
self._output = output self._output = output
@ -53,16 +50,16 @@ class EchoingStdin(object):
def make_input_stream(input, charset): def make_input_stream(input, charset):
# Is already an input stream. # Is already an input stream.
if hasattr(input, 'read'): if hasattr(input, "read"):
if PY2: if PY2:
return input return input
rv = _find_binary_reader(input) rv = _find_binary_reader(input)
if rv is not None: if rv is not None:
return rv return rv
raise TypeError('Could not find binary reader for input stream.') raise TypeError("Could not find binary reader for input stream.")
if input is None: if input is None:
input = b'' input = b""
elif not isinstance(input, bytes): elif not isinstance(input, bytes):
input = input.encode(charset) input = input.encode(charset)
if PY2: if PY2:
@ -73,13 +70,14 @@ def make_input_stream(input, charset):
class Result(object): class Result(object):
"""Holds the captured result of an invoked CLI script.""" """Holds the captured result of an invoked CLI script."""
def __init__(self, runner, stdout_bytes, stderr_bytes, exit_code, def __init__(
exception, exc_info=None): self, runner, stdout_bytes, stderr_bytes, exit_code, exception, exc_info=None
):
#: The runner that created the result #: The runner that created the result
self.runner = runner self.runner = runner
#: The standard output as bytes. #: The standard output as bytes.
self.stdout_bytes = stdout_bytes self.stdout_bytes = stdout_bytes
#: The standard error as bytes, or False(y) if not available #: The standard error as bytes, or None if not available
self.stderr_bytes = stderr_bytes self.stderr_bytes = stderr_bytes
#: The exit code as integer. #: The exit code as integer.
self.exit_code = exit_code self.exit_code = exit_code
@ -96,22 +94,22 @@ class Result(object):
@property @property
def stdout(self): def stdout(self):
"""The standard output as unicode string.""" """The standard output as unicode string."""
return self.stdout_bytes.decode(self.runner.charset, 'replace') \ return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
.replace('\r\n', '\n') "\r\n", "\n"
)
@property @property
def stderr(self): def stderr(self):
"""The standard error as unicode string.""" """The standard error as unicode string."""
if not self.stderr_bytes: if self.stderr_bytes is None:
raise ValueError("stderr not separately captured") raise ValueError("stderr not separately captured")
return self.stderr_bytes.decode(self.runner.charset, 'replace') \ return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
.replace('\r\n', '\n') "\r\n", "\n"
)
def __repr__(self): def __repr__(self):
return '<%s %s>' % ( return "<{} {}>".format(
type(self).__name__, type(self).__name__, repr(self.exception) if self.exception else "okay"
self.exception and repr(self.exception) or 'okay',
) )
@ -136,10 +134,9 @@ class CliRunner(object):
independently independently
""" """
def __init__(self, charset=None, env=None, echo_stdin=False, def __init__(self, charset=None, env=None, echo_stdin=False, mix_stderr=True):
mix_stderr=True):
if charset is None: if charset is None:
charset = 'utf-8' charset = "utf-8"
self.charset = charset self.charset = charset
self.env = env or {} self.env = env or {}
self.echo_stdin = echo_stdin self.echo_stdin = echo_stdin
@ -150,7 +147,7 @@ class CliRunner(object):
for it. The default is the `name` attribute or ``"root"`` if not for it. The default is the `name` attribute or ``"root"`` if not
set. set.
""" """
return cli.name or 'root' return cli.name or "root"
def make_env(self, overrides=None): def make_env(self, overrides=None):
"""Returns the environment overrides for invoking a script.""" """Returns the environment overrides for invoking a script."""
@ -182,8 +179,8 @@ class CliRunner(object):
old_stdin = sys.stdin old_stdin = sys.stdin
old_stdout = sys.stdout old_stdout = sys.stdout
old_stderr = sys.stderr old_stderr = sys.stderr
old_forced_width = clickpkg.formatting.FORCED_WIDTH old_forced_width = formatting.FORCED_WIDTH
clickpkg.formatting.FORCED_WIDTH = 80 formatting.FORCED_WIDTH = 80
env = self.make_env(env) env = self.make_env(env)
@ -200,12 +197,10 @@ class CliRunner(object):
if self.echo_stdin: if self.echo_stdin:
input = EchoingStdin(input, bytes_output) input = EchoingStdin(input, bytes_output)
input = io.TextIOWrapper(input, encoding=self.charset) input = io.TextIOWrapper(input, encoding=self.charset)
sys.stdout = io.TextIOWrapper( sys.stdout = io.TextIOWrapper(bytes_output, encoding=self.charset)
bytes_output, encoding=self.charset)
if not self.mix_stderr: if not self.mix_stderr:
bytes_error = io.BytesIO() bytes_error = io.BytesIO()
sys.stderr = io.TextIOWrapper( sys.stderr = io.TextIOWrapper(bytes_error, encoding=self.charset)
bytes_error, encoding=self.charset)
if self.mix_stderr: if self.mix_stderr:
sys.stderr = sys.stdout sys.stderr = sys.stdout
@ -213,16 +208,16 @@ class CliRunner(object):
sys.stdin = input sys.stdin = input
def visible_input(prompt=None): def visible_input(prompt=None):
sys.stdout.write(prompt or '') sys.stdout.write(prompt or "")
val = input.readline().rstrip('\r\n') val = input.readline().rstrip("\r\n")
sys.stdout.write(val + '\n') sys.stdout.write("{}\n".format(val))
sys.stdout.flush() sys.stdout.flush()
return val return val
def hidden_input(prompt=None): def hidden_input(prompt=None):
sys.stdout.write((prompt or '') + '\n') sys.stdout.write("{}\n".format(prompt or ""))
sys.stdout.flush() sys.stdout.flush()
return input.readline().rstrip('\r\n') return input.readline().rstrip("\r\n")
def _getchar(echo): def _getchar(echo):
char = sys.stdin.read(1) char = sys.stdin.read(1)
@ -238,14 +233,14 @@ class CliRunner(object):
return not default_color return not default_color
return not color return not color
old_visible_prompt_func = clickpkg.termui.visible_prompt_func old_visible_prompt_func = termui.visible_prompt_func
old_hidden_prompt_func = clickpkg.termui.hidden_prompt_func old_hidden_prompt_func = termui.hidden_prompt_func
old__getchar_func = clickpkg.termui._getchar old__getchar_func = termui._getchar
old_should_strip_ansi = clickpkg.utils.should_strip_ansi old_should_strip_ansi = utils.should_strip_ansi
clickpkg.termui.visible_prompt_func = visible_input termui.visible_prompt_func = visible_input
clickpkg.termui.hidden_prompt_func = hidden_input termui.hidden_prompt_func = hidden_input
clickpkg.termui._getchar = _getchar termui._getchar = _getchar
clickpkg.utils.should_strip_ansi = should_strip_ansi utils.should_strip_ansi = should_strip_ansi
old_env = {} old_env = {}
try: try:
@ -271,14 +266,22 @@ class CliRunner(object):
sys.stdout = old_stdout sys.stdout = old_stdout
sys.stderr = old_stderr sys.stderr = old_stderr
sys.stdin = old_stdin sys.stdin = old_stdin
clickpkg.termui.visible_prompt_func = old_visible_prompt_func termui.visible_prompt_func = old_visible_prompt_func
clickpkg.termui.hidden_prompt_func = old_hidden_prompt_func termui.hidden_prompt_func = old_hidden_prompt_func
clickpkg.termui._getchar = old__getchar_func termui._getchar = old__getchar_func
clickpkg.utils.should_strip_ansi = old_should_strip_ansi utils.should_strip_ansi = old_should_strip_ansi
clickpkg.formatting.FORCED_WIDTH = old_forced_width formatting.FORCED_WIDTH = old_forced_width
def invoke(self, cli, args=None, input=None, env=None, def invoke(
catch_exceptions=True, color=False, mix_stderr=False, **extra): self,
cli,
args=None,
input=None,
env=None,
catch_exceptions=True,
color=False,
**extra
):
"""Invokes a command in an isolated environment. The arguments are """Invokes a command in an isolated environment. The arguments are
forwarded directly to the command line script, the `extra` keyword forwarded directly to the command line script, the `extra` keyword
arguments are passed to the :meth:`~clickpkg.Command.main` function of arguments are passed to the :meth:`~clickpkg.Command.main` function of
@ -335,7 +338,7 @@ class CliRunner(object):
if not isinstance(exit_code, int): if not isinstance(exit_code, int):
sys.stdout.write(str(exit_code)) sys.stdout.write(str(exit_code))
sys.stdout.write('\n') sys.stdout.write("\n")
exit_code = 1 exit_code = 1
except Exception as e: except Exception as e:
@ -347,14 +350,19 @@ class CliRunner(object):
finally: finally:
sys.stdout.flush() sys.stdout.flush()
stdout = outstreams[0].getvalue() stdout = outstreams[0].getvalue()
stderr = outstreams[1] and outstreams[1].getvalue() if self.mix_stderr:
stderr = None
else:
stderr = outstreams[1].getvalue()
return Result(runner=self, return Result(
stdout_bytes=stdout, runner=self,
stderr_bytes=stderr, stdout_bytes=stdout,
exit_code=exit_code, stderr_bytes=stderr,
exception=exception, exit_code=exit_code,
exc_info=exc_info) exception=exception,
exc_info=exc_info,
)
@contextlib.contextmanager @contextlib.contextmanager
def isolated_filesystem(self): def isolated_filesystem(self):
@ -370,5 +378,5 @@ class CliRunner(object):
os.chdir(cwd) os.chdir(cwd)
try: try:
shutil.rmtree(t) shutil.rmtree(t)
except (OSError, IOError): except (OSError, IOError): # noqa: B014
pass pass

View file

@ -2,10 +2,16 @@ import os
import stat import stat
from datetime import datetime from datetime import datetime
from ._compat import open_stream, text_type, filename_to_ui, \ from ._compat import _get_argv_encoding
get_filesystem_encoding, get_streerror, _get_argv_encoding, PY2 from ._compat import filename_to_ui
from ._compat import get_filesystem_encoding
from ._compat import get_streerror
from ._compat import open_stream
from ._compat import PY2
from ._compat import text_type
from .exceptions import BadParameter from .exceptions import BadParameter
from .utils import safecall, LazyFile from .utils import LazyFile
from .utils import safecall
class ParamType(object): class ParamType(object):
@ -21,6 +27,7 @@ class ParamType(object):
This can be the case when the object is used with prompt This can be the case when the object is used with prompt
inputs. inputs.
""" """
is_composite = False is_composite = False
#: the descriptive name of this type #: the descriptive name of this type
@ -62,7 +69,7 @@ class ParamType(object):
then leading and trailing whitespace is ignored. Otherwise, leading then leading and trailing whitespace is ignored. Otherwise, leading
and trailing splitters usually lead to empty items being included. and trailing splitters usually lead to empty items being included.
""" """
return (rv or '').split(self.envvar_list_splitter) return (rv or "").split(self.envvar_list_splitter)
def fail(self, message, param=None, ctx=None): def fail(self, message, param=None, ctx=None):
"""Helper method to fail with an invalid value message.""" """Helper method to fail with an invalid value message."""
@ -78,7 +85,6 @@ class CompositeParamType(ParamType):
class FuncParamType(ParamType): class FuncParamType(ParamType):
def __init__(self, func): def __init__(self, func):
self.name = func.__name__ self.name = func.__name__
self.func = func self.func = func
@ -90,22 +96,22 @@ class FuncParamType(ParamType):
try: try:
value = text_type(value) value = text_type(value)
except UnicodeError: except UnicodeError:
value = str(value).decode('utf-8', 'replace') value = str(value).decode("utf-8", "replace")
self.fail(value, param, ctx) self.fail(value, param, ctx)
class UnprocessedParamType(ParamType): class UnprocessedParamType(ParamType):
name = 'text' name = "text"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
return value return value
def __repr__(self): def __repr__(self):
return 'UNPROCESSED' return "UNPROCESSED"
class StringParamType(ParamType): class StringParamType(ParamType):
name = 'text' name = "text"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
if isinstance(value, bytes): if isinstance(value, bytes):
@ -118,12 +124,14 @@ class StringParamType(ParamType):
try: try:
value = value.decode(fs_enc) value = value.decode(fs_enc)
except UnicodeError: except UnicodeError:
value = value.decode('utf-8', 'replace') value = value.decode("utf-8", "replace")
else:
value = value.decode("utf-8", "replace")
return value return value
return value return value
def __repr__(self): def __repr__(self):
return 'STRING' return "STRING"
class Choice(ParamType): class Choice(ParamType):
@ -133,54 +141,68 @@ class Choice(ParamType):
You should only pass a list or tuple of choices. Other iterables You should only pass a list or tuple of choices. Other iterables
(like generators) may lead to surprising results. (like generators) may lead to surprising results.
The resulting value will always be one of the originally passed choices
regardless of ``case_sensitive`` or any ``ctx.token_normalize_func``
being specified.
See :ref:`choice-opts` for an example. See :ref:`choice-opts` for an example.
:param case_sensitive: Set to false to make choices case :param case_sensitive: Set to false to make choices case
insensitive. Defaults to true. insensitive. Defaults to true.
""" """
name = 'choice' name = "choice"
def __init__(self, choices, case_sensitive=True): def __init__(self, choices, case_sensitive=True):
self.choices = choices self.choices = choices
self.case_sensitive = case_sensitive self.case_sensitive = case_sensitive
def get_metavar(self, param): def get_metavar(self, param):
return '[%s]' % '|'.join(self.choices) return "[{}]".format("|".join(self.choices))
def get_missing_message(self, param): def get_missing_message(self, param):
return 'Choose from:\n\t%s.' % ',\n\t'.join(self.choices) return "Choose from:\n\t{}.".format(",\n\t".join(self.choices))
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
# Exact match
if value in self.choices:
return value
# Match through normalization and case sensitivity # Match through normalization and case sensitivity
# first do token_normalize_func, then lowercase # first do token_normalize_func, then lowercase
# preserve original `value` to produce an accurate message in # preserve original `value` to produce an accurate message in
# `self.fail` # `self.fail`
normed_value = value normed_value = value
normed_choices = self.choices normed_choices = {choice: choice for choice in self.choices}
if ctx is not None and \ if ctx is not None and ctx.token_normalize_func is not None:
ctx.token_normalize_func is not None:
normed_value = ctx.token_normalize_func(value) normed_value = ctx.token_normalize_func(value)
normed_choices = [ctx.token_normalize_func(choice) for choice in normed_choices = {
self.choices] ctx.token_normalize_func(normed_choice): original
for normed_choice, original in normed_choices.items()
}
if not self.case_sensitive: if not self.case_sensitive:
normed_value = normed_value.lower() if PY2:
normed_choices = [choice.lower() for choice in normed_choices] lower = str.lower
else:
lower = str.casefold
normed_value = lower(normed_value)
normed_choices = {
lower(normed_choice): original
for normed_choice, original in normed_choices.items()
}
if normed_value in normed_choices: if normed_value in normed_choices:
return normed_value return normed_choices[normed_value]
self.fail('invalid choice: %s. (choose from %s)' % self.fail(
(value, ', '.join(self.choices)), param, ctx) "invalid choice: {}. (choose from {})".format(
value, ", ".join(self.choices)
),
param,
ctx,
)
def __repr__(self): def __repr__(self):
return 'Choice(%r)' % list(self.choices) return "Choice('{}')".format(list(self.choices))
class DateTime(ParamType): class DateTime(ParamType):
@ -203,17 +225,14 @@ class DateTime(ParamType):
``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
``'%Y-%m-%d %H:%M:%S'``. ``'%Y-%m-%d %H:%M:%S'``.
""" """
name = 'datetime'
name = "datetime"
def __init__(self, formats=None): def __init__(self, formats=None):
self.formats = formats or [ self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
'%Y-%m-%d',
'%Y-%m-%dT%H:%M:%S',
'%Y-%m-%d %H:%M:%S'
]
def get_metavar(self, param): def get_metavar(self, param):
return '[{}]'.format('|'.join(self.formats)) return "[{}]".format("|".join(self.formats))
def _try_to_convert_date(self, value, format): def _try_to_convert_date(self, value, format):
try: try:
@ -229,24 +248,26 @@ class DateTime(ParamType):
return dtime return dtime
self.fail( self.fail(
'invalid datetime format: {}. (choose from {})'.format( "invalid datetime format: {}. (choose from {})".format(
value, ', '.join(self.formats))) value, ", ".join(self.formats)
)
)
def __repr__(self): def __repr__(self):
return 'DateTime' return "DateTime"
class IntParamType(ParamType): class IntParamType(ParamType):
name = 'integer' name = "integer"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
try: try:
return int(value) return int(value)
except (ValueError, UnicodeError): except ValueError:
self.fail('%s is not a valid integer' % value, param, ctx) self.fail("{} is not a valid integer".format(value), param, ctx)
def __repr__(self): def __repr__(self):
return 'INT' return "INT"
class IntRange(IntParamType): class IntRange(IntParamType):
@ -257,7 +278,8 @@ class IntRange(IntParamType):
See :ref:`ranges` for an example. See :ref:`ranges` for an example.
""" """
name = 'integer range'
name = "integer range"
def __init__(self, min=None, max=None, clamp=False): def __init__(self, min=None, max=None, clamp=False):
self.min = min self.min = min
@ -271,35 +293,55 @@ class IntRange(IntParamType):
return self.min return self.min
if self.max is not None and rv > self.max: if self.max is not None and rv > self.max:
return self.max return self.max
if self.min is not None and rv < self.min or \ if (
self.max is not None and rv > self.max: self.min is not None
and rv < self.min
or self.max is not None
and rv > self.max
):
if self.min is None: if self.min is None:
self.fail('%s is bigger than the maximum valid value ' self.fail(
'%s.' % (rv, self.max), param, ctx) "{} is bigger than the maximum valid value {}.".format(
rv, self.max
),
param,
ctx,
)
elif self.max is None: elif self.max is None:
self.fail('%s is smaller than the minimum valid value ' self.fail(
'%s.' % (rv, self.min), param, ctx) "{} is smaller than the minimum valid value {}.".format(
rv, self.min
),
param,
ctx,
)
else: else:
self.fail('%s is not in the valid range of %s to %s.' self.fail(
% (rv, self.min, self.max), param, ctx) "{} is not in the valid range of {} to {}.".format(
rv, self.min, self.max
),
param,
ctx,
)
return rv return rv
def __repr__(self): def __repr__(self):
return 'IntRange(%r, %r)' % (self.min, self.max) return "IntRange({}, {})".format(self.min, self.max)
class FloatParamType(ParamType): class FloatParamType(ParamType):
name = 'float' name = "float"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
try: try:
return float(value) return float(value)
except (UnicodeError, ValueError): except ValueError:
self.fail('%s is not a valid floating point value' % self.fail(
value, param, ctx) "{} is not a valid floating point value".format(value), param, ctx
)
def __repr__(self): def __repr__(self):
return 'FLOAT' return "FLOAT"
class FloatRange(FloatParamType): class FloatRange(FloatParamType):
@ -310,7 +352,8 @@ class FloatRange(FloatParamType):
See :ref:`ranges` for an example. See :ref:`ranges` for an example.
""" """
name = 'float range'
name = "float range"
def __init__(self, min=None, max=None, clamp=False): def __init__(self, min=None, max=None, clamp=False):
self.min = min self.min = min
@ -324,54 +367,74 @@ class FloatRange(FloatParamType):
return self.min return self.min
if self.max is not None and rv > self.max: if self.max is not None and rv > self.max:
return self.max return self.max
if self.min is not None and rv < self.min or \ if (
self.max is not None and rv > self.max: self.min is not None
and rv < self.min
or self.max is not None
and rv > self.max
):
if self.min is None: if self.min is None:
self.fail('%s is bigger than the maximum valid value ' self.fail(
'%s.' % (rv, self.max), param, ctx) "{} is bigger than the maximum valid value {}.".format(
rv, self.max
),
param,
ctx,
)
elif self.max is None: elif self.max is None:
self.fail('%s is smaller than the minimum valid value ' self.fail(
'%s.' % (rv, self.min), param, ctx) "{} is smaller than the minimum valid value {}.".format(
rv, self.min
),
param,
ctx,
)
else: else:
self.fail('%s is not in the valid range of %s to %s.' self.fail(
% (rv, self.min, self.max), param, ctx) "{} is not in the valid range of {} to {}.".format(
rv, self.min, self.max
),
param,
ctx,
)
return rv return rv
def __repr__(self): def __repr__(self):
return 'FloatRange(%r, %r)' % (self.min, self.max) return "FloatRange({}, {})".format(self.min, self.max)
class BoolParamType(ParamType): class BoolParamType(ParamType):
name = 'boolean' name = "boolean"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
if isinstance(value, bool): if isinstance(value, bool):
return bool(value) return bool(value)
value = value.lower() value = value.lower()
if value in ('true', 't', '1', 'yes', 'y'): if value in ("true", "t", "1", "yes", "y"):
return True return True
elif value in ('false', 'f', '0', 'no', 'n'): elif value in ("false", "f", "0", "no", "n"):
return False return False
self.fail('%s is not a valid boolean' % value, param, ctx) self.fail("{} is not a valid boolean".format(value), param, ctx)
def __repr__(self): def __repr__(self):
return 'BOOL' return "BOOL"
class UUIDParameterType(ParamType): class UUIDParameterType(ParamType):
name = 'uuid' name = "uuid"
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
import uuid import uuid
try: try:
if PY2 and isinstance(value, text_type): if PY2 and isinstance(value, text_type):
value = value.encode('ascii') value = value.encode("ascii")
return uuid.UUID(value) return uuid.UUID(value)
except (UnicodeError, ValueError): except ValueError:
self.fail('%s is not a valid UUID value' % value, param, ctx) self.fail("{} is not a valid UUID value".format(value), param, ctx)
def __repr__(self): def __repr__(self):
return 'UUID' return "UUID"
class File(ParamType): class File(ParamType):
@ -400,11 +463,13 @@ class File(ParamType):
See :ref:`file-args` for more information. See :ref:`file-args` for more information.
""" """
name = 'filename'
name = "filename"
envvar_list_splitter = os.path.pathsep envvar_list_splitter = os.path.pathsep
def __init__(self, mode='r', encoding=None, errors='strict', lazy=None, def __init__(
atomic=False): self, mode="r", encoding=None, errors="strict", lazy=None, atomic=False
):
self.mode = mode self.mode = mode
self.encoding = encoding self.encoding = encoding
self.errors = errors self.errors = errors
@ -414,29 +479,30 @@ class File(ParamType):
def resolve_lazy_flag(self, value): def resolve_lazy_flag(self, value):
if self.lazy is not None: if self.lazy is not None:
return self.lazy return self.lazy
if value == '-': if value == "-":
return False return False
elif 'w' in self.mode: elif "w" in self.mode:
return True return True
return False return False
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
try: try:
if hasattr(value, 'read') or hasattr(value, 'write'): if hasattr(value, "read") or hasattr(value, "write"):
return value return value
lazy = self.resolve_lazy_flag(value) lazy = self.resolve_lazy_flag(value)
if lazy: if lazy:
f = LazyFile(value, self.mode, self.encoding, self.errors, f = LazyFile(
atomic=self.atomic) value, self.mode, self.encoding, self.errors, atomic=self.atomic
)
if ctx is not None: if ctx is not None:
ctx.call_on_close(f.close_intelligently) ctx.call_on_close(f.close_intelligently)
return f return f
f, should_close = open_stream(value, self.mode, f, should_close = open_stream(
self.encoding, self.errors, value, self.mode, self.encoding, self.errors, atomic=self.atomic
atomic=self.atomic) )
# If a context is provided, we automatically close the file # If a context is provided, we automatically close the file
# at the end of the context execution (or flush out). If a # at the end of the context execution (or flush out). If a
# context does not exist, it's the caller's responsibility to # context does not exist, it's the caller's responsibility to
@ -448,11 +514,14 @@ class File(ParamType):
else: else:
ctx.call_on_close(safecall(f.flush)) ctx.call_on_close(safecall(f.flush))
return f return f
except (IOError, OSError) as e: except (IOError, OSError) as e: # noqa: B014
self.fail('Could not open file: %s: %s' % ( self.fail(
filename_to_ui(value), "Could not open file: {}: {}".format(
get_streerror(e), filename_to_ui(value), get_streerror(e)
), param, ctx) ),
param,
ctx,
)
class Path(ParamType): class Path(ParamType):
@ -485,11 +554,20 @@ class Path(ParamType):
unicode depending on what makes most sense given the unicode depending on what makes most sense given the
input data Click deals with. input data Click deals with.
""" """
envvar_list_splitter = os.path.pathsep envvar_list_splitter = os.path.pathsep
def __init__(self, exists=False, file_okay=True, dir_okay=True, def __init__(
writable=False, readable=True, resolve_path=False, self,
allow_dash=False, path_type=None): exists=False,
file_okay=True,
dir_okay=True,
writable=False,
readable=True,
resolve_path=False,
allow_dash=False,
path_type=None,
):
self.exists = exists self.exists = exists
self.file_okay = file_okay self.file_okay = file_okay
self.dir_okay = dir_okay self.dir_okay = dir_okay
@ -500,14 +578,14 @@ class Path(ParamType):
self.type = path_type self.type = path_type
if self.file_okay and not self.dir_okay: if self.file_okay and not self.dir_okay:
self.name = 'file' self.name = "file"
self.path_type = 'File' self.path_type = "File"
elif self.dir_okay and not self.file_okay: elif self.dir_okay and not self.file_okay:
self.name = 'directory' self.name = "directory"
self.path_type = 'Directory' self.path_type = "Directory"
else: else:
self.name = 'path' self.name = "path"
self.path_type = 'Path' self.path_type = "Path"
def coerce_path_result(self, rv): def coerce_path_result(self, rv):
if self.type is not None and not isinstance(rv, self.type): if self.type is not None and not isinstance(rv, self.type):
@ -520,7 +598,7 @@ class Path(ParamType):
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
rv = value rv = value
is_dash = self.file_okay and self.allow_dash and rv in (b'-', '-') is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-")
if not is_dash: if not is_dash:
if self.resolve_path: if self.resolve_path:
@ -531,31 +609,44 @@ class Path(ParamType):
except OSError: except OSError:
if not self.exists: if not self.exists:
return self.coerce_path_result(rv) return self.coerce_path_result(rv)
self.fail('%s "%s" does not exist.' % ( self.fail(
self.path_type, "{} '{}' does not exist.".format(
filename_to_ui(value) self.path_type, filename_to_ui(value)
), param, ctx) ),
param,
ctx,
)
if not self.file_okay and stat.S_ISREG(st.st_mode): if not self.file_okay and stat.S_ISREG(st.st_mode):
self.fail('%s "%s" is a file.' % ( self.fail(
self.path_type, "{} '{}' is a file.".format(self.path_type, filename_to_ui(value)),
filename_to_ui(value) param,
), param, ctx) ctx,
)
if not self.dir_okay and stat.S_ISDIR(st.st_mode): if not self.dir_okay and stat.S_ISDIR(st.st_mode):
self.fail('%s "%s" is a directory.' % ( self.fail(
self.path_type, "{} '{}' is a directory.".format(
filename_to_ui(value) self.path_type, filename_to_ui(value)
), param, ctx) ),
param,
ctx,
)
if self.writable and not os.access(value, os.W_OK): if self.writable and not os.access(value, os.W_OK):
self.fail('%s "%s" is not writable.' % ( self.fail(
self.path_type, "{} '{}' is not writable.".format(
filename_to_ui(value) self.path_type, filename_to_ui(value)
), param, ctx) ),
param,
ctx,
)
if self.readable and not os.access(value, os.R_OK): if self.readable and not os.access(value, os.R_OK):
self.fail('%s "%s" is not readable.' % ( self.fail(
self.path_type, "{} '{}' is not readable.".format(
filename_to_ui(value) self.path_type, filename_to_ui(value)
), param, ctx) ),
param,
ctx,
)
return self.coerce_path_result(rv) return self.coerce_path_result(rv)
@ -579,7 +670,7 @@ class Tuple(CompositeParamType):
@property @property
def name(self): def name(self):
return "<" + " ".join(ty.name for ty in self.types) + ">" return "<{}>".format(" ".join(ty.name for ty in self.types))
@property @property
def arity(self): def arity(self):
@ -587,14 +678,16 @@ class Tuple(CompositeParamType):
def convert(self, value, param, ctx): def convert(self, value, param, ctx):
if len(value) != len(self.types): if len(value) != len(self.types):
raise TypeError('It would appear that nargs is set to conflict ' raise TypeError(
'with the composite type arity.') "It would appear that nargs is set to conflict with the"
" composite type arity."
)
return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value)) return tuple(ty(x, param, ctx) for ty, x in zip(self.types, value))
def convert_type(ty, default=None): def convert_type(ty, default=None):
"""Converts a callable or python ty into the most appropriate param """Converts a callable or python type into the most appropriate
ty. param type.
""" """
guessed_type = False guessed_type = False
if ty is None and default is not None: if ty is None and default is not None:
@ -627,8 +720,9 @@ def convert_type(ty, default=None):
if __debug__: if __debug__:
try: try:
if issubclass(ty, ParamType): if issubclass(ty, ParamType):
raise AssertionError('Attempted to use an uninstantiated ' raise AssertionError(
'parameter type (%s).' % ty) "Attempted to use an uninstantiated parameter type ({}).".format(ty)
)
except TypeError: except TypeError:
pass pass
return FuncParamType(ty) return FuncParamType(ty)

View file

@ -1,34 +1,47 @@
import os import os
import sys import sys
from ._compat import _default_text_stderr
from ._compat import _default_text_stdout
from ._compat import auto_wrap_for_ansi
from ._compat import binary_streams
from ._compat import filename_to_ui
from ._compat import get_filesystem_encoding
from ._compat import get_streerror
from ._compat import is_bytes
from ._compat import open_stream
from ._compat import PY2
from ._compat import should_strip_ansi
from ._compat import string_types
from ._compat import strip_ansi
from ._compat import text_streams
from ._compat import text_type
from ._compat import WIN
from .globals import resolve_color_default from .globals import resolve_color_default
from ._compat import text_type, open_stream, get_filesystem_encoding, \
get_streerror, string_types, PY2, binary_streams, text_streams, \
filename_to_ui, auto_wrap_for_ansi, strip_ansi, should_strip_ansi, \
_default_text_stdout, _default_text_stderr, is_bytes, WIN
if not PY2: if not PY2:
from ._compat import _find_binary_writer from ._compat import _find_binary_writer
elif WIN: elif WIN:
from ._winconsole import _get_windows_argv, \ from ._winconsole import _get_windows_argv
_hash_py_argv, _initial_argv_hash from ._winconsole import _hash_py_argv
from ._winconsole import _initial_argv_hash
echo_native_types = string_types + (bytes, bytearray) echo_native_types = string_types + (bytes, bytearray)
def _posixify(name): def _posixify(name):
return '-'.join(name.split()).lower() return "-".join(name.split()).lower()
def safecall(func): def safecall(func):
"""Wraps a function so that it swallows exceptions.""" """Wraps a function so that it swallows exceptions."""
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except Exception: except Exception:
pass pass
return wrapper return wrapper
@ -38,7 +51,7 @@ def make_str(value):
try: try:
return value.decode(get_filesystem_encoding()) return value.decode(get_filesystem_encoding())
except UnicodeError: except UnicodeError:
return value.decode('utf-8', 'replace') return value.decode("utf-8", "replace")
return text_type(value) return text_type(value)
@ -50,21 +63,21 @@ def make_default_short_help(help, max_length=45):
done = False done = False
for word in words: for word in words:
if word[-1:] == '.': if word[-1:] == ".":
done = True done = True
new_length = result and 1 + len(word) or len(word) new_length = 1 + len(word) if result else len(word)
if total_length + new_length > max_length: if total_length + new_length > max_length:
result.append('...') result.append("...")
done = True done = True
else: else:
if result: if result:
result.append(' ') result.append(" ")
result.append(word) result.append(word)
if done: if done:
break break
total_length += new_length total_length += new_length
return ''.join(result) return "".join(result)
class LazyFile(object): class LazyFile(object):
@ -74,19 +87,19 @@ class LazyFile(object):
files for writing. files for writing.
""" """
def __init__(self, filename, mode='r', encoding=None, errors='strict', def __init__(
atomic=False): self, filename, mode="r", encoding=None, errors="strict", atomic=False
):
self.name = filename self.name = filename
self.mode = mode self.mode = mode
self.encoding = encoding self.encoding = encoding
self.errors = errors self.errors = errors
self.atomic = atomic self.atomic = atomic
if filename == '-': if filename == "-":
self._f, self.should_close = open_stream(filename, mode, self._f, self.should_close = open_stream(filename, mode, encoding, errors)
encoding, errors)
else: else:
if 'r' in mode: if "r" in mode:
# Open and close the file in case we're opening it for # Open and close the file in case we're opening it for
# reading so that we can catch at least some errors in # reading so that we can catch at least some errors in
# some cases early. # some cases early.
@ -100,7 +113,7 @@ class LazyFile(object):
def __repr__(self): def __repr__(self):
if self._f is not None: if self._f is not None:
return repr(self._f) return repr(self._f)
return '<unopened file %r %s>' % (self.name, self.mode) return "<unopened file '{}' {}>".format(self.name, self.mode)
def open(self): def open(self):
"""Opens the file if it's not yet open. This call might fail with """Opens the file if it's not yet open. This call might fail with
@ -110,12 +123,12 @@ class LazyFile(object):
if self._f is not None: if self._f is not None:
return self._f return self._f
try: try:
rv, self.should_close = open_stream(self.name, self.mode, rv, self.should_close = open_stream(
self.encoding, self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
self.errors, )
atomic=self.atomic) except (IOError, OSError) as e: # noqa: E402
except (IOError, OSError) as e:
from .exceptions import FileError from .exceptions import FileError
raise FileError(self.name, hint=get_streerror(e)) raise FileError(self.name, hint=get_streerror(e))
self._f = rv self._f = rv
return rv return rv
@ -144,7 +157,6 @@ class LazyFile(object):
class KeepOpenFile(object): class KeepOpenFile(object):
def __init__(self, file): def __init__(self, file):
self._file = file self._file = file
@ -222,11 +234,11 @@ def echo(message=None, file=None, nl=True, err=False, color=None):
message = text_type(message) message = text_type(message)
if nl: if nl:
message = message or u'' message = message or u""
if isinstance(message, text_type): if isinstance(message, text_type):
message += u'\n' message += u"\n"
else: else:
message += b'\n' message += b"\n"
# If there is a message, and we're in Python 3, and the value looks # If there is a message, and we're in Python 3, and the value looks
# like bytes, we manually need to find the binary stream and write the # like bytes, we manually need to find the binary stream and write the
@ -273,11 +285,11 @@ def get_binary_stream(name):
""" """
opener = binary_streams.get(name) opener = binary_streams.get(name)
if opener is None: if opener is None:
raise TypeError('Unknown standard stream %r' % name) raise TypeError("Unknown standard stream '{}'".format(name))
return opener() return opener()
def get_text_stream(name, encoding=None, errors='strict'): def get_text_stream(name, encoding=None, errors="strict"):
"""Returns a system stream for text processing. This usually returns """Returns a system stream for text processing. This usually returns
a wrapped stream around a binary stream returned from a wrapped stream around a binary stream returned from
:func:`get_binary_stream` but it also can take shortcuts on Python 3 :func:`get_binary_stream` but it also can take shortcuts on Python 3
@ -290,12 +302,13 @@ def get_text_stream(name, encoding=None, errors='strict'):
""" """
opener = text_streams.get(name) opener = text_streams.get(name)
if opener is None: if opener is None:
raise TypeError('Unknown standard stream %r' % name) raise TypeError("Unknown standard stream '{}'".format(name))
return opener(encoding, errors) return opener(encoding, errors)
def open_file(filename, mode='r', encoding=None, errors='strict', def open_file(
lazy=False, atomic=False): filename, mode="r", encoding=None, errors="strict", lazy=False, atomic=False
):
"""This is similar to how the :class:`File` works but for manual """This is similar to how the :class:`File` works but for manual
usage. Files are opened non lazy by default. This can open regular usage. Files are opened non lazy by default. This can open regular
files as well as stdin/stdout if ``'-'`` is passed. files as well as stdin/stdout if ``'-'`` is passed.
@ -320,8 +333,7 @@ def open_file(filename, mode='r', encoding=None, errors='strict',
""" """
if lazy: if lazy:
return LazyFile(filename, mode, encoding, errors, atomic=atomic) return LazyFile(filename, mode, encoding, errors, atomic=atomic)
f, should_close = open_stream(filename, mode, encoding, errors, f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
atomic=atomic)
if not should_close: if not should_close:
f = KeepOpenFile(f) f = KeepOpenFile(f)
return f return f
@ -401,19 +413,21 @@ def get_app_dir(app_name, roaming=True, force_posix=False):
application support folder. application support folder.
""" """
if WIN: if WIN:
key = roaming and 'APPDATA' or 'LOCALAPPDATA' key = "APPDATA" if roaming else "LOCALAPPDATA"
folder = os.environ.get(key) folder = os.environ.get(key)
if folder is None: if folder is None:
folder = os.path.expanduser('~') folder = os.path.expanduser("~")
return os.path.join(folder, app_name) return os.path.join(folder, app_name)
if force_posix: if force_posix:
return os.path.join(os.path.expanduser('~/.' + _posixify(app_name))) return os.path.join(os.path.expanduser("~/.{}".format(_posixify(app_name))))
if sys.platform == 'darwin': if sys.platform == "darwin":
return os.path.join(os.path.expanduser( return os.path.join(
'~/Library/Application Support'), app_name) os.path.expanduser("~/Library/Application Support"), app_name
)
return os.path.join( return os.path.join(
os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')), os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
_posixify(app_name)) _posixify(app_name),
)
class PacifyFlushWrapper(object): class PacifyFlushWrapper(object):
@ -433,6 +447,7 @@ class PacifyFlushWrapper(object):
self.wrapped.flush() self.wrapped.flush()
except IOError as e: except IOError as e:
import errno import errno
if e.errno != errno.EPIPE: if e.errno != errno.EPIPE:
raise raise

View file

@ -1,8 +1,8 @@
from click.testing import CliRunner
import pytest import pytest
from click.testing import CliRunner
@pytest.fixture(scope='function')
@pytest.fixture(scope="function")
def runner(request): def runner(request):
return CliRunner() return CliRunner()

View file

@ -1,51 +1,46 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
import pytest import pytest
import click import click
from click._compat import PY2 from click._compat import PY2
from click._compat import text_type
def test_nargs_star(runner): def test_nargs_star(runner):
@click.command() @click.command()
@click.argument('src', nargs=-1) @click.argument("src", nargs=-1)
@click.argument('dst') @click.argument("dst")
def copy(src, dst): def copy(src, dst):
click.echo('src=%s' % '|'.join(src)) click.echo("src={}".format("|".join(src)))
click.echo('dst=%s' % dst) click.echo("dst={}".format(dst))
result = runner.invoke(copy, ['foo.txt', 'bar.txt', 'dir']) result = runner.invoke(copy, ["foo.txt", "bar.txt", "dir"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["src=foo.txt|bar.txt", "dst=dir"]
'src=foo.txt|bar.txt',
'dst=dir',
]
def test_nargs_default(runner): def test_nargs_default(runner):
try: with pytest.raises(TypeError, match="nargs=-1"):
@click.command() @click.command()
@click.argument('src', nargs=-1, default=42) @click.argument("src", nargs=-1, default=42)
def copy(src): def copy(src):
pass pass
except TypeError as e:
assert 'nargs=-1' in str(e)
else:
assert False
def test_nargs_tup(runner): def test_nargs_tup(runner):
@click.command() @click.command()
@click.argument('name', nargs=1) @click.argument("name", nargs=1)
@click.argument('point', nargs=2, type=click.INT) @click.argument("point", nargs=2, type=click.INT)
def copy(name, point): def copy(name, point):
click.echo('name=%s' % name) click.echo("name={}".format(name))
click.echo('point=%d/%d' % point) click.echo("point={0[0]}/{0[1]}".format(point))
result = runner.invoke(copy, ['peter', '1', '2']) result = runner.invoke(copy, ["peter", "1", "2"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["name=peter", "point=1/2"]
'name=peter',
'point=1/2',
]
def test_nargs_tup_composite(runner): def test_nargs_tup_composite(runner):
@ -57,37 +52,61 @@ def test_nargs_tup_composite(runner):
] ]
for opts in variations: for opts in variations:
@click.command()
@click.argument('item', **opts)
def copy(item):
click.echo('name=%s id=%d' % item)
result = runner.invoke(copy, ['peter', '1']) @click.command()
@click.argument("item", **opts)
def copy(item):
click.echo("name={0[0]} id={0[1]:d}".format(item))
result = runner.invoke(copy, ["peter", "1"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["name=peter id=1"]
'name=peter id=1',
]
def test_nargs_err(runner): def test_nargs_err(runner):
@click.command() @click.command()
@click.argument('x') @click.argument("x")
def copy(x): def copy(x):
click.echo(x) click.echo(x)
result = runner.invoke(copy, ['foo']) result = runner.invoke(copy, ["foo"])
assert not result.exception assert not result.exception
assert result.output == 'foo\n' assert result.output == "foo\n"
result = runner.invoke(copy, ['foo', 'bar']) result = runner.invoke(copy, ["foo", "bar"])
assert result.exit_code == 2 assert result.exit_code == 2
assert 'Got unexpected extra argument (bar)' in result.output assert "Got unexpected extra argument (bar)" in result.output
def test_bytes_args(runner, monkeypatch):
@click.command()
@click.argument("arg")
def from_bytes(arg):
assert isinstance(
arg, text_type
), "UTF-8 encoded argument should be implicitly converted to Unicode"
# Simulate empty locale environment variables
if PY2:
monkeypatch.setattr(sys.stdin, "encoding", "ANSI_X3.4-1968")
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "ANSI_X3.4-1968")
monkeypatch.setattr(sys, "getdefaultencoding", lambda: "ascii")
else:
monkeypatch.setattr(sys.stdin, "encoding", "utf-8")
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "utf-8")
monkeypatch.setattr(sys, "getdefaultencoding", lambda: "utf-8")
runner.invoke(
from_bytes,
[u"Something outside of ASCII range: 林".encode("UTF-8")],
catch_exceptions=False,
)
def test_file_args(runner): def test_file_args(runner):
@click.command() @click.command()
@click.argument('input', type=click.File('rb')) @click.argument("input", type=click.File("rb"))
@click.argument('output', type=click.File('wb')) @click.argument("output", type=click.File("wb"))
def inout(input, output): def inout(input, output):
while True: while True:
chunk = input.read(1024) chunk = input.read(1024)
@ -96,203 +115,201 @@ def test_file_args(runner):
output.write(chunk) output.write(chunk)
with runner.isolated_filesystem(): with runner.isolated_filesystem():
result = runner.invoke(inout, ['-', 'hello.txt'], input='Hey!') result = runner.invoke(inout, ["-", "hello.txt"], input="Hey!")
assert result.output == '' assert result.output == ""
assert result.exit_code == 0 assert result.exit_code == 0
with open('hello.txt', 'rb') as f: with open("hello.txt", "rb") as f:
assert f.read() == b'Hey!' assert f.read() == b"Hey!"
result = runner.invoke(inout, ['hello.txt', '-']) result = runner.invoke(inout, ["hello.txt", "-"])
assert result.output == 'Hey!' assert result.output == "Hey!"
assert result.exit_code == 0 assert result.exit_code == 0
def test_path_args(runner): def test_path_args(runner):
@click.command() @click.command()
@click.argument('input', type=click.Path(dir_okay=False, allow_dash=True)) @click.argument("input", type=click.Path(dir_okay=False, allow_dash=True))
def foo(input): def foo(input):
click.echo(input) click.echo(input)
result = runner.invoke(foo, ['-']) result = runner.invoke(foo, ["-"])
assert result.output == '-\n' assert result.output == "-\n"
assert result.exit_code == 0 assert result.exit_code == 0
def test_file_atomics(runner): def test_file_atomics(runner):
@click.command() @click.command()
@click.argument('output', type=click.File('wb', atomic=True)) @click.argument("output", type=click.File("wb", atomic=True))
def inout(output): def inout(output):
output.write(b'Foo bar baz\n') output.write(b"Foo bar baz\n")
output.flush() output.flush()
with open(output.name, 'rb') as f: with open(output.name, "rb") as f:
old_content = f.read() old_content = f.read()
assert old_content == b'OLD\n' assert old_content == b"OLD\n"
with runner.isolated_filesystem(): with runner.isolated_filesystem():
with open('foo.txt', 'wb') as f: with open("foo.txt", "wb") as f:
f.write(b'OLD\n') f.write(b"OLD\n")
result = runner.invoke(inout, ['foo.txt'], input='Hey!', result = runner.invoke(inout, ["foo.txt"], input="Hey!", catch_exceptions=False)
catch_exceptions=False) assert result.output == ""
assert result.output == ''
assert result.exit_code == 0 assert result.exit_code == 0
with open('foo.txt', 'rb') as f: with open("foo.txt", "rb") as f:
assert f.read() == b'Foo bar baz\n' assert f.read() == b"Foo bar baz\n"
def test_stdout_default(runner): def test_stdout_default(runner):
@click.command() @click.command()
@click.argument('output', type=click.File('w'), default='-') @click.argument("output", type=click.File("w"), default="-")
def inout(output): def inout(output):
output.write('Foo bar baz\n') output.write("Foo bar baz\n")
output.flush() output.flush()
result = runner.invoke(inout, []) result = runner.invoke(inout, [])
assert not result.exception assert not result.exception
assert result.output == 'Foo bar baz\n' assert result.output == "Foo bar baz\n"
def test_nargs_envvar(runner): def test_nargs_envvar(runner):
@click.command() @click.command()
@click.option('--arg', nargs=2) @click.option("--arg", nargs=2)
def cmd(arg): def cmd(arg):
click.echo('|'.join(arg)) click.echo("|".join(arg))
result = runner.invoke(cmd, [], auto_envvar_prefix='TEST', result = runner.invoke(
env={'TEST_ARG': 'foo bar'}) cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar"}
)
assert not result.exception assert not result.exception
assert result.output == 'foo|bar\n' assert result.output == "foo|bar\n"
@click.command() @click.command()
@click.option('--arg', envvar='X', nargs=2) @click.option("--arg", envvar="X", nargs=2)
def cmd(arg): def cmd(arg):
click.echo('|'.join(arg)) click.echo("|".join(arg))
result = runner.invoke(cmd, [], env={'X': 'foo bar'}) result = runner.invoke(cmd, [], env={"X": "foo bar"})
assert not result.exception assert not result.exception
assert result.output == 'foo|bar\n' assert result.output == "foo|bar\n"
def test_empty_nargs(runner): def test_empty_nargs(runner):
@click.command() @click.command()
@click.argument('arg', nargs=-1) @click.argument("arg", nargs=-1)
def cmd(arg): def cmd(arg):
click.echo('arg:' + '|'.join(arg)) click.echo("arg:{}".format("|".join(arg)))
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'arg:\n' assert result.output == "arg:\n"
@click.command() @click.command()
@click.argument('arg', nargs=-1, required=True) @click.argument("arg", nargs=-1, required=True)
def cmd2(arg): def cmd2(arg):
click.echo('arg:' + '|'.join(arg)) click.echo("arg:{}".format("|".join(arg)))
result = runner.invoke(cmd2, []) result = runner.invoke(cmd2, [])
assert result.exit_code == 2 assert result.exit_code == 2
assert 'Missing argument "ARG..."' in result.output assert "Missing argument 'ARG...'" in result.output
def test_missing_arg(runner): def test_missing_arg(runner):
@click.command() @click.command()
@click.argument('arg') @click.argument("arg")
def cmd(arg): def cmd(arg):
click.echo('arg:' + arg) click.echo("arg:{}".format(arg))
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.exit_code == 2 assert result.exit_code == 2
assert 'Missing argument "ARG".' in result.output assert "Missing argument 'ARG'." in result.output
def test_missing_argument_string_cast():
ctx = click.Context(click.Command(""))
with pytest.raises(click.MissingParameter) as excinfo:
click.Argument(["a"], required=True).full_process_value(ctx, None)
assert str(excinfo.value) == "missing parameter: a"
def test_implicit_non_required(runner): def test_implicit_non_required(runner):
@click.command() @click.command()
@click.argument('f', default='test') @click.argument("f", default="test")
def cli(f): def cli(f):
click.echo(f) click.echo(f)
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'test\n' assert result.output == "test\n"
def test_eat_options(runner): def test_eat_options(runner):
@click.command() @click.command()
@click.option('-f') @click.option("-f")
@click.argument('files', nargs=-1) @click.argument("files", nargs=-1)
def cmd(f, files): def cmd(f, files):
for filename in files: for filename in files:
click.echo(filename) click.echo(filename)
click.echo(f) click.echo(f)
result = runner.invoke(cmd, ['--', '-foo', 'bar']) result = runner.invoke(cmd, ["--", "-foo", "bar"])
assert result.output.splitlines() == [ assert result.output.splitlines() == ["-foo", "bar", ""]
'-foo',
'bar',
'',
]
result = runner.invoke(cmd, ['-f', '-x', '--', '-foo', 'bar']) result = runner.invoke(cmd, ["-f", "-x", "--", "-foo", "bar"])
assert result.output.splitlines() == [ assert result.output.splitlines() == ["-foo", "bar", "-x"]
'-foo',
'bar',
'-x',
]
def test_nargs_star_ordering(runner): def test_nargs_star_ordering(runner):
@click.command() @click.command()
@click.argument('a', nargs=-1) @click.argument("a", nargs=-1)
@click.argument('b') @click.argument("b")
@click.argument('c') @click.argument("c")
def cmd(a, b, c): def cmd(a, b, c):
for arg in (a, b, c): for arg in (a, b, c):
click.echo(arg) click.echo(arg)
result = runner.invoke(cmd, ['a', 'b', 'c']) result = runner.invoke(cmd, ["a", "b", "c"])
assert result.output.splitlines() == [ assert result.output.splitlines() == ["(u'a',)" if PY2 else "('a',)", "b", "c"]
PY2 and "(u'a',)" or "('a',)",
'b',
'c',
]
def test_nargs_specified_plus_star_ordering(runner): def test_nargs_specified_plus_star_ordering(runner):
@click.command() @click.command()
@click.argument('a', nargs=-1) @click.argument("a", nargs=-1)
@click.argument('b') @click.argument("b")
@click.argument('c', nargs=2) @click.argument("c", nargs=2)
def cmd(a, b, c): def cmd(a, b, c):
for arg in (a, b, c): for arg in (a, b, c):
click.echo(arg) click.echo(arg)
result = runner.invoke(cmd, ['a', 'b', 'c', 'd', 'e', 'f']) result = runner.invoke(cmd, ["a", "b", "c", "d", "e", "f"])
assert result.output.splitlines() == [ assert result.output.splitlines() == [
PY2 and "(u'a', u'b', u'c')" or "('a', 'b', 'c')", "(u'a', u'b', u'c')" if PY2 else "('a', 'b', 'c')",
'd', "d",
PY2 and "(u'e', u'f')" or "('e', 'f')", "(u'e', u'f')" if PY2 else "('e', 'f')",
] ]
def test_defaults_for_nargs(runner): def test_defaults_for_nargs(runner):
@click.command() @click.command()
@click.argument('a', nargs=2, type=int, default=(1, 2)) @click.argument("a", nargs=2, type=int, default=(1, 2))
def cmd(a): def cmd(a):
x, y = a x, y = a
click.echo(x + y) click.echo(x + y)
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.output.strip() == '3' assert result.output.strip() == "3"
result = runner.invoke(cmd, ['3', '4']) result = runner.invoke(cmd, ["3", "4"])
assert result.output.strip() == '7' assert result.output.strip() == "7"
result = runner.invoke(cmd, ['3']) result = runner.invoke(cmd, ["3"])
assert result.exception is not None assert result.exception is not None
assert 'argument a takes 2 values' in result.output assert "argument a takes 2 values" in result.output
def test_multiple_param_decls_not_allowed(runner): def test_multiple_param_decls_not_allowed(runner):
with pytest.raises(TypeError): with pytest.raises(TypeError):
@click.command() @click.command()
@click.argument('x', click.Choice(['a', 'b'])) @click.argument("x", click.Choice(["a", "b"]))
def copy(x): def copy(x):
click.echo(x) click.echo(x)

View file

@ -1,98 +1,100 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pytest
import click import click
from click._bashcomplete import get_choices from click._bashcomplete import get_choices
def choices_without_help(cli, args, incomplete): def choices_without_help(cli, args, incomplete):
completions = get_choices(cli, 'dummy', args, incomplete) completions = get_choices(cli, "dummy", args, incomplete)
return [c[0] for c in completions] return [c[0] for c in completions]
def choices_with_help(cli, args, incomplete): def choices_with_help(cli, args, incomplete):
return list(get_choices(cli, 'dummy', args, incomplete)) return list(get_choices(cli, "dummy", args, incomplete))
def test_single_command(): def test_single_command():
@click.command() @click.command()
@click.option('--local-opt') @click.option("--local-opt")
def cli(local_opt): def cli(local_opt):
pass pass
assert choices_without_help(cli, [], '-') == ['--local-opt'] assert choices_without_help(cli, [], "-") == ["--local-opt"]
assert choices_without_help(cli, [], '') == [] assert choices_without_help(cli, [], "") == []
def test_boolean_flag(): def test_boolean_flag():
@click.command() @click.command()
@click.option('--shout/--no-shout', default=False) @click.option("--shout/--no-shout", default=False)
def cli(local_opt): def cli(local_opt):
pass pass
assert choices_without_help(cli, [], '-') == ['--shout', '--no-shout'] assert choices_without_help(cli, [], "-") == ["--shout", "--no-shout"]
def test_multi_value_option(): def test_multi_value_option():
@click.group() @click.group()
@click.option('--pos', nargs=2, type=float) @click.option("--pos", nargs=2, type=float)
def cli(local_opt): def cli(local_opt):
pass pass
@cli.command() @cli.command()
@click.option('--local-opt') @click.option("--local-opt")
def sub(local_opt): def sub(local_opt):
pass pass
assert choices_without_help(cli, [], '-') == ['--pos'] assert choices_without_help(cli, [], "-") == ["--pos"]
assert choices_without_help(cli, ['--pos'], '') == [] assert choices_without_help(cli, ["--pos"], "") == []
assert choices_without_help(cli, ['--pos', '1.0'], '') == [] assert choices_without_help(cli, ["--pos", "1.0"], "") == []
assert choices_without_help(cli, ['--pos', '1.0', '1.0'], '') == ['sub'] assert choices_without_help(cli, ["--pos", "1.0", "1.0"], "") == ["sub"]
def test_multi_option(): def test_multi_option():
@click.command() @click.command()
@click.option('--message', '-m', multiple=True) @click.option("--message", "-m", multiple=True)
def cli(local_opt): def cli(local_opt):
pass pass
assert choices_without_help(cli, [], '-') == ['--message', '-m'] assert choices_without_help(cli, [], "-") == ["--message", "-m"]
assert choices_without_help(cli, ['-m'], '') == [] assert choices_without_help(cli, ["-m"], "") == []
def test_small_chain(): def test_small_chain():
@click.group() @click.group()
@click.option('--global-opt') @click.option("--global-opt")
def cli(global_opt): def cli(global_opt):
pass pass
@cli.command() @cli.command()
@click.option('--local-opt') @click.option("--local-opt")
def sub(local_opt): def sub(local_opt):
pass pass
assert choices_without_help(cli, [], '') == ['sub'] assert choices_without_help(cli, [], "") == ["sub"]
assert choices_without_help(cli, [], '-') == ['--global-opt'] assert choices_without_help(cli, [], "-") == ["--global-opt"]
assert choices_without_help(cli, ['sub'], '') == [] assert choices_without_help(cli, ["sub"], "") == []
assert choices_without_help(cli, ['sub'], '-') == ['--local-opt'] assert choices_without_help(cli, ["sub"], "-") == ["--local-opt"]
def test_long_chain(): def test_long_chain():
@click.group('cli') @click.group("cli")
@click.option('--cli-opt') @click.option("--cli-opt")
def cli(cli_opt): def cli(cli_opt):
pass pass
@cli.group('asub') @cli.group("asub")
@click.option('--asub-opt') @click.option("--asub-opt")
def asub(asub_opt): def asub(asub_opt):
pass pass
@asub.group('bsub') @asub.group("bsub")
@click.option('--bsub-opt') @click.option("--bsub-opt")
def bsub(bsub_opt): def bsub(bsub_opt):
pass pass
COLORS = ['red', 'green', 'blue'] COLORS = ["red", "green", "blue"]
def get_colors(ctx, args, incomplete): def get_colors(ctx, args, incomplete):
for c in COLORS: for c in COLORS:
if c.startswith(incomplete): if c.startswith(incomplete):
@ -103,197 +105,237 @@ def test_long_chain():
if incomplete in c: if incomplete in c:
yield c yield c
CSUB_OPT_CHOICES = ['foo', 'bar'] CSUB_OPT_CHOICES = ["foo", "bar"]
CSUB_CHOICES = ['bar', 'baz'] CSUB_CHOICES = ["bar", "baz"]
@bsub.command('csub')
@click.option('--csub-opt', type=click.Choice(CSUB_OPT_CHOICES)) @bsub.command("csub")
@click.option('--csub', type=click.Choice(CSUB_CHOICES)) @click.option("--csub-opt", type=click.Choice(CSUB_OPT_CHOICES))
@click.option('--search-color', autocompletion=search_colors) @click.option("--csub", type=click.Choice(CSUB_CHOICES))
@click.argument('color', autocompletion=get_colors) @click.option("--search-color", autocompletion=search_colors)
@click.argument("color", autocompletion=get_colors)
def csub(csub_opt, color): def csub(csub_opt, color):
pass pass
assert choices_without_help(cli, [], '-') == ['--cli-opt'] assert choices_without_help(cli, [], "-") == ["--cli-opt"]
assert choices_without_help(cli, [], '') == ['asub'] assert choices_without_help(cli, [], "") == ["asub"]
assert choices_without_help(cli, ['asub'], '-') == ['--asub-opt'] assert choices_without_help(cli, ["asub"], "-") == ["--asub-opt"]
assert choices_without_help(cli, ['asub'], '') == ['bsub'] assert choices_without_help(cli, ["asub"], "") == ["bsub"]
assert choices_without_help(cli, ['asub', 'bsub'], '-') == ['--bsub-opt'] assert choices_without_help(cli, ["asub", "bsub"], "-") == ["--bsub-opt"]
assert choices_without_help(cli, ['asub', 'bsub'], '') == ['csub'] assert choices_without_help(cli, ["asub", "bsub"], "") == ["csub"]
assert choices_without_help(cli, ['asub', 'bsub', 'csub'], '-') == ['--csub-opt', '--csub', '--search-color'] assert choices_without_help(cli, ["asub", "bsub", "csub"], "-") == [
assert choices_without_help(cli, ['asub', 'bsub', 'csub', '--csub-opt'], '') == CSUB_OPT_CHOICES "--csub-opt",
assert choices_without_help(cli, ['asub', 'bsub', 'csub'], '--csub') == ['--csub-opt', '--csub'] "--csub",
assert choices_without_help(cli, ['asub', 'bsub', 'csub', '--csub'], '') == CSUB_CHOICES "--search-color",
assert choices_without_help(cli, ['asub', 'bsub', 'csub', '--csub-opt'], 'f') == ['foo'] ]
assert choices_without_help(cli, ['asub', 'bsub', 'csub'], '') == COLORS assert (
assert choices_without_help(cli, ['asub', 'bsub', 'csub'], 'b') == ['blue'] choices_without_help(cli, ["asub", "bsub", "csub", "--csub-opt"], "")
assert choices_without_help(cli, ['asub', 'bsub', 'csub', '--search-color'], 'een') == ['green'] == CSUB_OPT_CHOICES
)
assert choices_without_help(cli, ["asub", "bsub", "csub"], "--csub") == [
"--csub-opt",
"--csub",
]
assert (
choices_without_help(cli, ["asub", "bsub", "csub", "--csub"], "")
== CSUB_CHOICES
)
assert choices_without_help(cli, ["asub", "bsub", "csub", "--csub-opt"], "f") == [
"foo"
]
assert choices_without_help(cli, ["asub", "bsub", "csub"], "") == COLORS
assert choices_without_help(cli, ["asub", "bsub", "csub"], "b") == ["blue"]
assert choices_without_help(
cli, ["asub", "bsub", "csub", "--search-color"], "een"
) == ["green"]
def test_chaining(): def test_chaining():
@click.group('cli', chain=True) @click.group("cli", chain=True)
@click.option('--cli-opt') @click.option("--cli-opt")
@click.argument('arg', type=click.Choice(['cliarg1', 'cliarg2'])) @click.argument("arg", type=click.Choice(["cliarg1", "cliarg2"]))
def cli(cli_opt, arg): def cli(cli_opt, arg):
pass pass
@cli.command() @cli.command()
@click.option('--asub-opt') @click.option("--asub-opt")
def asub(asub_opt): def asub(asub_opt):
pass pass
@cli.command(help='bsub help') @cli.command(help="bsub help")
@click.option('--bsub-opt') @click.option("--bsub-opt")
@click.argument('arg', type=click.Choice(['arg1', 'arg2'])) @click.argument("arg", type=click.Choice(["arg1", "arg2"]))
def bsub(bsub_opt, arg): def bsub(bsub_opt, arg):
pass pass
@cli.command() @cli.command()
@click.option('--csub-opt') @click.option("--csub-opt")
@click.argument('arg', type=click.Choice(['carg1', 'carg2']), default='carg1') @click.argument("arg", type=click.Choice(["carg1", "carg2"]), default="carg1")
def csub(csub_opt, arg): def csub(csub_opt, arg):
pass pass
assert choices_without_help(cli, [], '-') == ['--cli-opt'] assert choices_without_help(cli, [], "-") == ["--cli-opt"]
assert choices_without_help(cli, [], '') == ['cliarg1', 'cliarg2'] assert choices_without_help(cli, [], "") == ["cliarg1", "cliarg2"]
assert choices_without_help(cli, ['cliarg1', 'asub'], '-') == ['--asub-opt'] assert choices_without_help(cli, ["cliarg1", "asub"], "-") == ["--asub-opt"]
assert choices_without_help(cli, ['cliarg1', 'asub'], '') == ['bsub', 'csub'] assert choices_without_help(cli, ["cliarg1", "asub"], "") == ["bsub", "csub"]
assert choices_without_help(cli, ['cliarg1', 'bsub'], '') == ['arg1', 'arg2'] assert choices_without_help(cli, ["cliarg1", "bsub"], "") == ["arg1", "arg2"]
assert choices_without_help(cli, ['cliarg1', 'asub', '--asub-opt'], '') == [] assert choices_without_help(cli, ["cliarg1", "asub", "--asub-opt"], "") == []
assert choices_without_help(cli, ['cliarg1', 'asub', '--asub-opt', '5', 'bsub'], '-') == ['--bsub-opt'] assert choices_without_help(
assert choices_without_help(cli, ['cliarg1', 'asub', 'bsub'], '-') == ['--bsub-opt'] cli, ["cliarg1", "asub", "--asub-opt", "5", "bsub"], "-"
assert choices_without_help(cli, ['cliarg1', 'asub', 'csub'], '') == ['carg1', 'carg2'] ) == ["--bsub-opt"]
assert choices_without_help(cli, ['cliarg1', 'bsub', 'arg1', 'csub'], '') == ['carg1', 'carg2'] assert choices_without_help(cli, ["cliarg1", "asub", "bsub"], "-") == ["--bsub-opt"]
assert choices_without_help(cli, ['cliarg1', 'asub', 'csub'], '-') == ['--csub-opt'] assert choices_without_help(cli, ["cliarg1", "asub", "csub"], "") == [
assert choices_with_help(cli, ['cliarg1', 'asub'], 'b') == [('bsub', 'bsub help')] "carg1",
"carg2",
]
assert choices_without_help(cli, ["cliarg1", "bsub", "arg1", "csub"], "") == [
"carg1",
"carg2",
]
assert choices_without_help(cli, ["cliarg1", "asub", "csub"], "-") == ["--csub-opt"]
assert choices_with_help(cli, ["cliarg1", "asub"], "b") == [("bsub", "bsub help")]
def test_argument_choice(): def test_argument_choice():
@click.command() @click.command()
@click.argument('arg1', required=True, type=click.Choice(['arg11', 'arg12'])) @click.argument("arg1", required=True, type=click.Choice(["arg11", "arg12"]))
@click.argument('arg2', type=click.Choice(['arg21', 'arg22']), default='arg21') @click.argument("arg2", type=click.Choice(["arg21", "arg22"]), default="arg21")
@click.argument('arg3', type=click.Choice(['arg', 'argument']), default='arg') @click.argument("arg3", type=click.Choice(["arg", "argument"]), default="arg")
def cli(): def cli():
pass pass
assert choices_without_help(cli, [], '') == ['arg11', 'arg12'] assert choices_without_help(cli, [], "") == ["arg11", "arg12"]
assert choices_without_help(cli, [], 'arg') == ['arg11', 'arg12'] assert choices_without_help(cli, [], "arg") == ["arg11", "arg12"]
assert choices_without_help(cli, ['arg11'], '') == ['arg21', 'arg22'] assert choices_without_help(cli, ["arg11"], "") == ["arg21", "arg22"]
assert choices_without_help(cli, ['arg12', 'arg21'], '') == ['arg', 'argument'] assert choices_without_help(cli, ["arg12", "arg21"], "") == ["arg", "argument"]
assert choices_without_help(cli, ['arg12', 'arg21'], 'argu') == ['argument'] assert choices_without_help(cli, ["arg12", "arg21"], "argu") == ["argument"]
def test_option_choice(): def test_option_choice():
@click.command() @click.command()
@click.option('--opt1', type=click.Choice(['opt11', 'opt12']), help='opt1 help') @click.option("--opt1", type=click.Choice(["opt11", "opt12"]), help="opt1 help")
@click.option('--opt2', type=click.Choice(['opt21', 'opt22']), default='opt21') @click.option("--opt2", type=click.Choice(["opt21", "opt22"]), default="opt21")
@click.option('--opt3', type=click.Choice(['opt', 'option'])) @click.option("--opt3", type=click.Choice(["opt", "option"]))
def cli(): def cli():
pass pass
assert choices_with_help(cli, [], '-') == [('--opt1', 'opt1 help'), assert choices_with_help(cli, [], "-") == [
('--opt2', None), ("--opt1", "opt1 help"),
('--opt3', None)] ("--opt2", None),
assert choices_without_help(cli, [], '--opt') == ['--opt1', '--opt2', '--opt3'] ("--opt3", None),
assert choices_without_help(cli, [], '--opt1=') == ['opt11', 'opt12'] ]
assert choices_without_help(cli, [], '--opt2=') == ['opt21', 'opt22'] assert choices_without_help(cli, [], "--opt") == ["--opt1", "--opt2", "--opt3"]
assert choices_without_help(cli, ['--opt2'], '=') == ['opt21', 'opt22'] assert choices_without_help(cli, [], "--opt1=") == ["opt11", "opt12"]
assert choices_without_help(cli, ['--opt2', '='], 'opt') == ['opt21', 'opt22'] assert choices_without_help(cli, [], "--opt2=") == ["opt21", "opt22"]
assert choices_without_help(cli, ['--opt1'], '') == ['opt11', 'opt12'] assert choices_without_help(cli, ["--opt2"], "=") == ["opt21", "opt22"]
assert choices_without_help(cli, ['--opt2'], '') == ['opt21', 'opt22'] assert choices_without_help(cli, ["--opt2", "="], "opt") == ["opt21", "opt22"]
assert choices_without_help(cli, ['--opt1', 'opt11', '--opt2'], '') == ['opt21', 'opt22'] assert choices_without_help(cli, ["--opt1"], "") == ["opt11", "opt12"]
assert choices_without_help(cli, ['--opt2', 'opt21'], '-') == ['--opt1', '--opt3'] assert choices_without_help(cli, ["--opt2"], "") == ["opt21", "opt22"]
assert choices_without_help(cli, ['--opt1', 'opt11'], '-') == ['--opt2', '--opt3'] assert choices_without_help(cli, ["--opt1", "opt11", "--opt2"], "") == [
assert choices_without_help(cli, ['--opt1'], 'opt') == ['opt11', 'opt12'] "opt21",
assert choices_without_help(cli, ['--opt3'], 'opti') == ['option'] "opt22",
]
assert choices_without_help(cli, ["--opt2", "opt21"], "-") == ["--opt1", "--opt3"]
assert choices_without_help(cli, ["--opt1", "opt11"], "-") == ["--opt2", "--opt3"]
assert choices_without_help(cli, ["--opt1"], "opt") == ["opt11", "opt12"]
assert choices_without_help(cli, ["--opt3"], "opti") == ["option"]
assert choices_without_help(cli, ['--opt1', 'invalid_opt'], '-') == ['--opt2', '--opt3'] assert choices_without_help(cli, ["--opt1", "invalid_opt"], "-") == [
"--opt2",
"--opt3",
]
def test_option_and_arg_choice(): def test_option_and_arg_choice():
@click.command() @click.command()
@click.option('--opt1', type=click.Choice(['opt11', 'opt12'])) @click.option("--opt1", type=click.Choice(["opt11", "opt12"]))
@click.argument('arg1', required=False, type=click.Choice(['arg11', 'arg12'])) @click.argument("arg1", required=False, type=click.Choice(["arg11", "arg12"]))
@click.option('--opt2', type=click.Choice(['opt21', 'opt22'])) @click.option("--opt2", type=click.Choice(["opt21", "opt22"]))
def cli(): def cli():
pass pass
assert choices_without_help(cli, ['--opt1'], '') == ['opt11', 'opt12'] assert choices_without_help(cli, ["--opt1"], "") == ["opt11", "opt12"]
assert choices_without_help(cli, [''], '--opt1=') == ['opt11', 'opt12'] assert choices_without_help(cli, [""], "--opt1=") == ["opt11", "opt12"]
assert choices_without_help(cli, [], '') == ['arg11', 'arg12'] assert choices_without_help(cli, [], "") == ["arg11", "arg12"]
assert choices_without_help(cli, ['--opt2'], '') == ['opt21', 'opt22'] assert choices_without_help(cli, ["--opt2"], "") == ["opt21", "opt22"]
assert choices_without_help(cli, ['arg11'], '--opt') == ['--opt1', '--opt2'] assert choices_without_help(cli, ["arg11"], "--opt") == ["--opt1", "--opt2"]
assert choices_without_help(cli, [], '--opt') == ['--opt1', '--opt2'] assert choices_without_help(cli, [], "--opt") == ["--opt1", "--opt2"]
def test_boolean_flag_choice(): def test_boolean_flag_choice():
@click.command() @click.command()
@click.option('--shout/--no-shout', default=False) @click.option("--shout/--no-shout", default=False)
@click.argument('arg', required=False, type=click.Choice(['arg1', 'arg2'])) @click.argument("arg", required=False, type=click.Choice(["arg1", "arg2"]))
def cli(local_opt): def cli(local_opt):
pass pass
assert choices_without_help(cli, [], '-') == ['--shout', '--no-shout'] assert choices_without_help(cli, [], "-") == ["--shout", "--no-shout"]
assert choices_without_help(cli, ['--shout'], '') == ['arg1', 'arg2'] assert choices_without_help(cli, ["--shout"], "") == ["arg1", "arg2"]
def test_multi_value_option_choice(): def test_multi_value_option_choice():
@click.command() @click.command()
@click.option('--pos', nargs=2, type=click.Choice(['pos1', 'pos2'])) @click.option("--pos", nargs=2, type=click.Choice(["pos1", "pos2"]))
@click.argument('arg', required=False, type=click.Choice(['arg1', 'arg2'])) @click.argument("arg", required=False, type=click.Choice(["arg1", "arg2"]))
def cli(local_opt): def cli(local_opt):
pass pass
assert choices_without_help(cli, ['--pos'], '') == ['pos1', 'pos2'] assert choices_without_help(cli, ["--pos"], "") == ["pos1", "pos2"]
assert choices_without_help(cli, ['--pos', 'pos1'], '') == ['pos1', 'pos2'] assert choices_without_help(cli, ["--pos", "pos1"], "") == ["pos1", "pos2"]
assert choices_without_help(cli, ['--pos', 'pos1', 'pos2'], '') == ['arg1', 'arg2'] assert choices_without_help(cli, ["--pos", "pos1", "pos2"], "") == ["arg1", "arg2"]
assert choices_without_help(cli, ['--pos', 'pos1', 'pos2', 'arg1'], '') == [] assert choices_without_help(cli, ["--pos", "pos1", "pos2", "arg1"], "") == []
def test_multi_option_choice(): def test_multi_option_choice():
@click.command() @click.command()
@click.option('--message', '-m', multiple=True, type=click.Choice(['m1', 'm2'])) @click.option("--message", "-m", multiple=True, type=click.Choice(["m1", "m2"]))
@click.argument('arg', required=False, type=click.Choice(['arg1', 'arg2'])) @click.argument("arg", required=False, type=click.Choice(["arg1", "arg2"]))
def cli(local_opt): def cli(local_opt):
pass pass
assert choices_without_help(cli, ['-m'], '') == ['m1', 'm2'] assert choices_without_help(cli, ["-m"], "") == ["m1", "m2"]
assert choices_without_help(cli, ['-m', 'm1', '-m'], '') == ['m1', 'm2'] assert choices_without_help(cli, ["-m", "m1", "-m"], "") == ["m1", "m2"]
assert choices_without_help(cli, ['-m', 'm1'], '') == ['arg1', 'arg2'] assert choices_without_help(cli, ["-m", "m1"], "") == ["arg1", "arg2"]
def test_variadic_argument_choice(): def test_variadic_argument_choice():
@click.command() @click.command()
@click.option('--opt', type=click.Choice(['opt1', 'opt2'])) @click.option("--opt", type=click.Choice(["opt1", "opt2"]))
@click.argument('src', nargs=-1, type=click.Choice(['src1', 'src2'])) @click.argument("src", nargs=-1, type=click.Choice(["src1", "src2"]))
def cli(local_opt): def cli(local_opt):
pass pass
assert choices_without_help(cli, ['src1', 'src2'], '') == ['src1', 'src2'] assert choices_without_help(cli, ["src1", "src2"], "") == ["src1", "src2"]
assert choices_without_help(cli, ['src1', 'src2'], '--o') == ['--opt'] assert choices_without_help(cli, ["src1", "src2"], "--o") == ["--opt"]
assert choices_without_help(cli, ['src1', 'src2', '--opt'], '') == ['opt1', 'opt2'] assert choices_without_help(cli, ["src1", "src2", "--opt"], "") == ["opt1", "opt2"]
assert choices_without_help(cli, ['src1', 'src2'], '') == ['src1', 'src2'] assert choices_without_help(cli, ["src1", "src2"], "") == ["src1", "src2"]
def test_variadic_argument_complete(): def test_variadic_argument_complete():
def _complete(ctx, args, incomplete): def _complete(ctx, args, incomplete):
return ['abc', 'def', 'ghi', 'jkl', 'mno', 'pqr', 'stu', 'vwx', 'yz'] return ["abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz"]
@click.group() @click.group()
def entrypoint(): def entrypoint():
pass pass
@click.command() @click.command()
@click.option('--opt', autocompletion=_complete) @click.option("--opt", autocompletion=_complete)
@click.argument('arg', nargs=-1) @click.argument("arg", nargs=-1)
def subcommand(opt, arg): def subcommand(opt, arg):
pass pass
entrypoint.add_command(subcommand) entrypoint.add_command(subcommand)
assert choices_without_help(entrypoint, ['subcommand', '--opt'], '') == _complete(0,0,0) assert choices_without_help(entrypoint, ["subcommand", "--opt"], "") == _complete(
assert choices_without_help(entrypoint, ['subcommand', 'whatever', '--opt'], '') == _complete(0,0,0) 0, 0, 0
assert choices_without_help(entrypoint, ['subcommand', 'whatever', '--opt', 'abc'], '') == [] )
assert choices_without_help(
entrypoint, ["subcommand", "whatever", "--opt"], ""
) == _complete(0, 0, 0)
assert (
choices_without_help(entrypoint, ["subcommand", "whatever", "--opt", "abc"], "")
== []
)
def test_long_chain_choice(): def test_long_chain_choice():
@ -302,19 +344,25 @@ def test_long_chain_choice():
pass pass
@cli.group() @cli.group()
@click.option('--sub-opt', type=click.Choice(['subopt1', 'subopt2'])) @click.option("--sub-opt", type=click.Choice(["subopt1", "subopt2"]))
@click.argument('sub-arg', required=False, type=click.Choice(['subarg1', 'subarg2'])) @click.argument(
"sub-arg", required=False, type=click.Choice(["subarg1", "subarg2"])
)
def sub(sub_opt, sub_arg): def sub(sub_opt, sub_arg):
pass pass
@sub.command(short_help='bsub help') @sub.command(short_help="bsub help")
@click.option('--bsub-opt', type=click.Choice(['bsubopt1', 'bsubopt2'])) @click.option("--bsub-opt", type=click.Choice(["bsubopt1", "bsubopt2"]))
@click.argument('bsub-arg1', required=False, type=click.Choice(['bsubarg1', 'bsubarg2'])) @click.argument(
@click.argument('bbsub-arg2', required=False, type=click.Choice(['bbsubarg1', 'bbsubarg2'])) "bsub-arg1", required=False, type=click.Choice(["bsubarg1", "bsubarg2"])
)
@click.argument(
"bbsub-arg2", required=False, type=click.Choice(["bbsubarg1", "bbsubarg2"])
)
def bsub(bsub_opt): def bsub(bsub_opt):
pass pass
@sub.group('csub') @sub.group("csub")
def csub(): def csub():
pass pass
@ -322,24 +370,42 @@ def test_long_chain_choice():
def dsub(): def dsub():
pass pass
assert choices_with_help(cli, ['sub', 'subarg1'], '') == [('bsub', 'bsub help'), ('csub', '')] assert choices_with_help(cli, ["sub", "subarg1"], "") == [
assert choices_without_help(cli, ['sub'], '') == ['subarg1', 'subarg2'] ("bsub", "bsub help"),
assert choices_without_help(cli, ['sub', '--sub-opt'], '') == ['subopt1', 'subopt2'] ("csub", ""),
assert choices_without_help(cli, ['sub', '--sub-opt', 'subopt1'], '') == \ ]
['subarg1', 'subarg2'] assert choices_without_help(cli, ["sub"], "") == ["subarg1", "subarg2"]
assert choices_without_help(cli, assert choices_without_help(cli, ["sub", "--sub-opt"], "") == ["subopt1", "subopt2"]
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub'], '-') == ['--bsub-opt'] assert choices_without_help(cli, ["sub", "--sub-opt", "subopt1"], "") == [
assert choices_without_help(cli, "subarg1",
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub'], '') == ['bsubarg1', 'bsubarg2'] "subarg2",
assert choices_without_help(cli, ]
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub', '--bsub-opt'], '') == \ assert choices_without_help(
['bsubopt1', 'bsubopt2'] cli, ["sub", "--sub-opt", "subopt1", "subarg1", "bsub"], "-"
assert choices_without_help(cli, ) == ["--bsub-opt"]
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub', '--bsub-opt', 'bsubopt1', 'bsubarg1'], assert choices_without_help(
'') == ['bbsubarg1', 'bbsubarg2'] cli, ["sub", "--sub-opt", "subopt1", "subarg1", "bsub"], ""
assert choices_without_help(cli, ) == ["bsubarg1", "bsubarg2"]
['sub', '--sub-opt', 'subopt1', 'subarg1', 'csub'], assert choices_without_help(
'') == ['dsub'] cli, ["sub", "--sub-opt", "subopt1", "subarg1", "bsub", "--bsub-opt"], ""
) == ["bsubopt1", "bsubopt2"]
assert choices_without_help(
cli,
[
"sub",
"--sub-opt",
"subopt1",
"subarg1",
"bsub",
"--bsub-opt",
"bsubopt1",
"bsubarg1",
],
"",
) == ["bbsubarg1", "bbsubarg2"]
assert choices_without_help(
cli, ["sub", "--sub-opt", "subopt1", "subarg1", "csub"], ""
) == ["dsub"]
def test_chained_multi(): def test_chained_multi():
@ -367,16 +433,16 @@ def test_chained_multi():
def esub(): def esub():
pass pass
assert choices_without_help(cli, ['sub'], '') == ['bsub', 'csub'] assert choices_without_help(cli, ["sub"], "") == ["bsub", "csub"]
assert choices_without_help(cli, ['sub'], 'c') == ['csub'] assert choices_without_help(cli, ["sub"], "c") == ["csub"]
assert choices_without_help(cli, ['sub', 'csub'], '') == ['dsub', 'esub'] assert choices_without_help(cli, ["sub", "csub"], "") == ["dsub", "esub"]
assert choices_without_help(cli, ['sub', 'csub', 'dsub'], '') == ['esub'] assert choices_without_help(cli, ["sub", "csub", "dsub"], "") == ["esub"]
def test_hidden(): def test_hidden():
@click.group() @click.group()
@click.option('--name', hidden=True) @click.option("--name", hidden=True)
@click.option('--choices', type=click.Choice([1, 2]), hidden=True) @click.option("--choices", type=click.Choice([1, 2]), hidden=True)
def cli(name): def cli(name):
pass pass
@ -393,17 +459,42 @@ def test_hidden():
pass pass
@cli.command(hidden=True) @cli.command(hidden=True)
@click.option('--hname') @click.option("--hname")
def hsub(): def hsub():
pass pass
assert choices_without_help(cli, [], '--n') == [] assert choices_without_help(cli, [], "--n") == []
assert choices_without_help(cli, [], '--c') == [] assert choices_without_help(cli, [], "--c") == []
# If the user exactly types out the hidden param, complete its options. # If the user exactly types out the hidden param, complete its options.
assert choices_without_help(cli, ['--choices'], '') == [1, 2] assert choices_without_help(cli, ["--choices"], "") == [1, 2]
assert choices_without_help(cli, [], '') == ['asub'] assert choices_without_help(cli, [], "") == ["asub"]
assert choices_without_help(cli, [], '') == ['asub'] assert choices_without_help(cli, [], "") == ["asub"]
assert choices_without_help(cli, [], 'h') == [] assert choices_without_help(cli, [], "h") == []
# If the user exactly types out the hidden command, complete its subcommands. # If the user exactly types out the hidden command, complete its subcommands.
assert choices_without_help(cli, ['hgroup'], '') == ['hgroupsub'] assert choices_without_help(cli, ["hgroup"], "") == ["hgroupsub"]
assert choices_without_help(cli, ['hsub'], '--h') == ['--hname'] assert choices_without_help(cli, ["hsub"], "--h") == ["--hname"]
@pytest.mark.parametrize(
("args", "part", "expect"),
[
([], "-", ["--opt"]),
(["value"], "--", ["--opt"]),
([], "-o", []),
(["--opt"], "-o", []),
(["--"], "", ["name", "-o", "--opt", "--"]),
(["--"], "--o", ["--opt"]),
],
)
def test_args_with_double_dash_complete(args, part, expect):
def _complete(ctx, args, incomplete):
values = ["name", "-o", "--opt", "--"]
return [x for x in values if x.startswith(incomplete)]
@click.command()
@click.option("--opt")
@click.argument("args", nargs=-1, autocompletion=_complete)
def cli(opt, args):
pass
assert choices_without_help(cli, args, part) == expect

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import uuid import uuid
import click import click
@ -8,443 +9,468 @@ def test_basic_functionality(runner):
@click.command() @click.command()
def cli(): def cli():
"""Hello World!""" """Hello World!"""
click.echo('I EXECUTED') click.echo("I EXECUTED")
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert not result.exception assert not result.exception
assert 'Hello World!' in result.output assert "Hello World!" in result.output
assert 'Show this message and exit.' in result.output assert "Show this message and exit." in result.output
assert result.exit_code == 0 assert result.exit_code == 0
assert 'I EXECUTED' not in result.output assert "I EXECUTED" not in result.output
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert 'I EXECUTED' in result.output assert "I EXECUTED" in result.output
assert result.exit_code == 0 assert result.exit_code == 0
def test_repr():
@click.command()
def command():
pass
@click.group()
def group():
pass
@group.command()
def subcommand():
pass
assert repr(command) == "<Command command>"
assert repr(group) == "<Group group>"
assert repr(subcommand) == "<Command subcommand>"
def test_return_values(): def test_return_values():
@click.command() @click.command()
def cli(): def cli():
return 42 return 42
with cli.make_context('foo', []) as ctx: with cli.make_context("foo", []) as ctx:
rv = cli.invoke(ctx) rv = cli.invoke(ctx)
assert rv is 42 assert rv == 42
def test_basic_group(runner): def test_basic_group(runner):
@click.group() @click.group()
def cli(): def cli():
"""This is the root.""" """This is the root."""
click.echo('ROOT EXECUTED') click.echo("ROOT EXECUTED")
@cli.command() @cli.command()
def subcommand(): def subcommand():
"""This is a subcommand.""" """This is a subcommand."""
click.echo('SUBCOMMAND EXECUTED') click.echo("SUBCOMMAND EXECUTED")
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert not result.exception assert not result.exception
assert 'COMMAND [ARGS]...' in result.output assert "COMMAND [ARGS]..." in result.output
assert 'This is the root' in result.output assert "This is the root" in result.output
assert 'This is a subcommand.' in result.output assert "This is a subcommand." in result.output
assert result.exit_code == 0 assert result.exit_code == 0
assert 'ROOT EXECUTED' not in result.output assert "ROOT EXECUTED" not in result.output
result = runner.invoke(cli, ['subcommand']) result = runner.invoke(cli, ["subcommand"])
assert not result.exception assert not result.exception
assert result.exit_code == 0 assert result.exit_code == 0
assert 'ROOT EXECUTED' in result.output assert "ROOT EXECUTED" in result.output
assert 'SUBCOMMAND EXECUTED' in result.output assert "SUBCOMMAND EXECUTED" in result.output
def test_basic_option(runner): def test_basic_option(runner):
@click.command() @click.command()
@click.option('--foo', default='no value') @click.option("--foo", default="no value")
def cli(foo): def cli(foo):
click.echo('FOO:[%s]' % foo) click.echo(u"FOO:[{}]".format(foo))
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert 'FOO:[no value]' in result.output assert "FOO:[no value]" in result.output
result = runner.invoke(cli, ['--foo=42']) result = runner.invoke(cli, ["--foo=42"])
assert not result.exception assert not result.exception
assert 'FOO:[42]' in result.output assert "FOO:[42]" in result.output
result = runner.invoke(cli, ['--foo']) result = runner.invoke(cli, ["--foo"])
assert result.exception assert result.exception
assert '--foo option requires an argument' in result.output assert "--foo option requires an argument" in result.output
result = runner.invoke(cli, ['--foo=']) result = runner.invoke(cli, ["--foo="])
assert not result.exception assert not result.exception
assert 'FOO:[]' in result.output assert "FOO:[]" in result.output
result = runner.invoke(cli, [u'--foo=\N{SNOWMAN}']) result = runner.invoke(cli, [u"--foo=\N{SNOWMAN}"])
assert not result.exception assert not result.exception
assert u'FOO:[\N{SNOWMAN}]' in result.output assert u"FOO:[\N{SNOWMAN}]" in result.output
def test_int_option(runner): def test_int_option(runner):
@click.command() @click.command()
@click.option('--foo', default=42) @click.option("--foo", default=42)
def cli(foo): def cli(foo):
click.echo('FOO:[%s]' % (foo * 2)) click.echo("FOO:[{}]".format(foo * 2))
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert 'FOO:[84]' in result.output assert "FOO:[84]" in result.output
result = runner.invoke(cli, ['--foo=23']) result = runner.invoke(cli, ["--foo=23"])
assert not result.exception assert not result.exception
assert 'FOO:[46]' in result.output assert "FOO:[46]" in result.output
result = runner.invoke(cli, ['--foo=bar']) result = runner.invoke(cli, ["--foo=bar"])
assert result.exception assert result.exception
assert 'Invalid value for "--foo": bar is not a valid integer' \ assert "Invalid value for '--foo': bar is not a valid integer" in result.output
in result.output
def test_uuid_option(runner): def test_uuid_option(runner):
@click.command() @click.command()
@click.option('--u', default='ba122011-349f-423b-873b-9d6a79c688ab', @click.option(
type=click.UUID) "--u", default="ba122011-349f-423b-873b-9d6a79c688ab", type=click.UUID
)
def cli(u): def cli(u):
assert type(u) is uuid.UUID assert type(u) is uuid.UUID
click.echo('U:[%s]' % u) click.echo("U:[{}]".format(u))
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert 'U:[ba122011-349f-423b-873b-9d6a79c688ab]' in result.output assert "U:[ba122011-349f-423b-873b-9d6a79c688ab]" in result.output
result = runner.invoke(cli, ['--u=821592c1-c50e-4971-9cd6-e89dc6832f86']) result = runner.invoke(cli, ["--u=821592c1-c50e-4971-9cd6-e89dc6832f86"])
assert not result.exception assert not result.exception
assert 'U:[821592c1-c50e-4971-9cd6-e89dc6832f86]' in result.output assert "U:[821592c1-c50e-4971-9cd6-e89dc6832f86]" in result.output
result = runner.invoke(cli, ['--u=bar']) result = runner.invoke(cli, ["--u=bar"])
assert result.exception assert result.exception
assert 'Invalid value for "--u": bar is not a valid UUID value' \ assert "Invalid value for '--u': bar is not a valid UUID value" in result.output
in result.output
def test_float_option(runner): def test_float_option(runner):
@click.command() @click.command()
@click.option('--foo', default=42, type=click.FLOAT) @click.option("--foo", default=42, type=click.FLOAT)
def cli(foo): def cli(foo):
assert type(foo) is float assert type(foo) is float
click.echo('FOO:[%s]' % foo) click.echo("FOO:[{}]".format(foo))
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert 'FOO:[42.0]' in result.output assert "FOO:[42.0]" in result.output
result = runner.invoke(cli, ['--foo=23.5']) result = runner.invoke(cli, ["--foo=23.5"])
assert not result.exception assert not result.exception
assert 'FOO:[23.5]' in result.output assert "FOO:[23.5]" in result.output
result = runner.invoke(cli, ['--foo=bar']) result = runner.invoke(cli, ["--foo=bar"])
assert result.exception assert result.exception
assert 'Invalid value for "--foo": bar is not a valid float' \ assert "Invalid value for '--foo': bar is not a valid float" in result.output
in result.output
def test_boolean_option(runner): def test_boolean_option(runner):
for default in True, False: for default in True, False:
@click.command() @click.command()
@click.option('--with-foo/--without-foo', default=default) @click.option("--with-foo/--without-foo", default=default)
def cli(with_foo): def cli(with_foo):
click.echo(with_foo) click.echo(with_foo)
result = runner.invoke(cli, ['--with-foo']) result = runner.invoke(cli, ["--with-foo"])
assert not result.exception assert not result.exception
assert result.output == 'True\n' assert result.output == "True\n"
result = runner.invoke(cli, ['--without-foo']) result = runner.invoke(cli, ["--without-foo"])
assert not result.exception assert not result.exception
assert result.output == 'False\n' assert result.output == "False\n"
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == '%s\n' % default assert result.output == "{}\n".format(default)
for default in True, False: for default in True, False:
@click.command() @click.command()
@click.option('--flag', is_flag=True, default=default) @click.option("--flag", is_flag=True, default=default)
def cli(flag): def cli(flag):
click.echo(flag) click.echo(flag)
result = runner.invoke(cli, ['--flag']) result = runner.invoke(cli, ["--flag"])
assert not result.exception assert not result.exception
assert result.output == '%s\n' % (not default) assert result.output == "{}\n".format(not default)
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == '%s\n' % (default) assert result.output == "{}\n".format(default)
def test_boolean_conversion(runner): def test_boolean_conversion(runner):
for default in True, False: for default in True, False:
@click.command() @click.command()
@click.option('--flag', default=default, type=bool) @click.option("--flag", default=default, type=bool)
def cli(flag): def cli(flag):
click.echo(flag) click.echo(flag)
for value in 'true', 't', '1', 'yes', 'y': for value in "true", "t", "1", "yes", "y":
result = runner.invoke(cli, ['--flag', value]) result = runner.invoke(cli, ["--flag", value])
assert not result.exception assert not result.exception
assert result.output == 'True\n' assert result.output == "True\n"
for value in 'false', 'f', '0', 'no', 'n': for value in "false", "f", "0", "no", "n":
result = runner.invoke(cli, ['--flag', value]) result = runner.invoke(cli, ["--flag", value])
assert not result.exception assert not result.exception
assert result.output == 'False\n' assert result.output == "False\n"
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == '%s\n' % default assert result.output == "{}\n".format(default)
def test_file_option(runner): def test_file_option(runner):
@click.command() @click.command()
@click.option('--file', type=click.File('w')) @click.option("--file", type=click.File("w"))
def input(file): def input(file):
file.write('Hello World!\n') file.write("Hello World!\n")
@click.command() @click.command()
@click.option('--file', type=click.File('r')) @click.option("--file", type=click.File("r"))
def output(file): def output(file):
click.echo(file.read()) click.echo(file.read())
with runner.isolated_filesystem(): with runner.isolated_filesystem():
result_in = runner.invoke(input, ['--file=example.txt']) result_in = runner.invoke(input, ["--file=example.txt"])
result_out = runner.invoke(output, ['--file=example.txt']) result_out = runner.invoke(output, ["--file=example.txt"])
assert not result_in.exception assert not result_in.exception
assert result_in.output == '' assert result_in.output == ""
assert not result_out.exception assert not result_out.exception
assert result_out.output == 'Hello World!\n\n' assert result_out.output == "Hello World!\n\n"
def test_file_lazy_mode(runner): def test_file_lazy_mode(runner):
do_io = False do_io = False
@click.command() @click.command()
@click.option('--file', type=click.File('w')) @click.option("--file", type=click.File("w"))
def input(file): def input(file):
if do_io: if do_io:
file.write('Hello World!\n') file.write("Hello World!\n")
@click.command() @click.command()
@click.option('--file', type=click.File('r')) @click.option("--file", type=click.File("r"))
def output(file): def output(file):
pass pass
with runner.isolated_filesystem(): with runner.isolated_filesystem():
os.mkdir('example.txt') os.mkdir("example.txt")
do_io = True do_io = True
result_in = runner.invoke(input, ['--file=example.txt']) result_in = runner.invoke(input, ["--file=example.txt"])
assert result_in.exit_code == 1 assert result_in.exit_code == 1
do_io = False do_io = False
result_in = runner.invoke(input, ['--file=example.txt']) result_in = runner.invoke(input, ["--file=example.txt"])
assert result_in.exit_code == 0 assert result_in.exit_code == 0
result_out = runner.invoke(output, ['--file=example.txt']) result_out = runner.invoke(output, ["--file=example.txt"])
assert result_out.exception assert result_out.exception
@click.command() @click.command()
@click.option('--file', type=click.File('w', lazy=False)) @click.option("--file", type=click.File("w", lazy=False))
def input_non_lazy(file): def input_non_lazy(file):
file.write('Hello World!\n') file.write("Hello World!\n")
with runner.isolated_filesystem(): with runner.isolated_filesystem():
os.mkdir('example.txt') os.mkdir("example.txt")
result_in = runner.invoke(input_non_lazy, ['--file=example.txt']) result_in = runner.invoke(input_non_lazy, ["--file=example.txt"])
assert result_in.exit_code == 2 assert result_in.exit_code == 2
assert 'Invalid value for "--file": Could not open file: example.txt' \ assert (
"Invalid value for '--file': Could not open file: example.txt"
in result_in.output in result_in.output
)
def test_path_option(runner): def test_path_option(runner):
@click.command() @click.command()
@click.option('-O', type=click.Path(file_okay=False, exists=True, @click.option("-O", type=click.Path(file_okay=False, exists=True, writable=True))
writable=True))
def write_to_dir(o): def write_to_dir(o):
with open(os.path.join(o, 'foo.txt'), 'wb') as f: with open(os.path.join(o, "foo.txt"), "wb") as f:
f.write(b'meh\n') f.write(b"meh\n")
with runner.isolated_filesystem(): with runner.isolated_filesystem():
os.mkdir('test') os.mkdir("test")
result = runner.invoke(write_to_dir, ['-O', 'test']) result = runner.invoke(write_to_dir, ["-O", "test"])
assert not result.exception assert not result.exception
with open('test/foo.txt', 'rb') as f: with open("test/foo.txt", "rb") as f:
assert f.read() == b'meh\n' assert f.read() == b"meh\n"
result = runner.invoke(write_to_dir, ['-O', 'test/foo.txt']) result = runner.invoke(write_to_dir, ["-O", "test/foo.txt"])
assert 'Invalid value for "-O": Directory "test/foo.txt" is a file.' \ assert "is a file" in result.output
in result.output
@click.command() @click.command()
@click.option('-f', type=click.Path(exists=True)) @click.option("-f", type=click.Path(exists=True))
def showtype(f): def showtype(f):
click.echo('is_file=%s' % os.path.isfile(f)) click.echo("is_file={}".format(os.path.isfile(f)))
click.echo('is_dir=%s' % os.path.isdir(f)) click.echo("is_dir={}".format(os.path.isdir(f)))
with runner.isolated_filesystem(): with runner.isolated_filesystem():
result = runner.invoke(showtype, ['-f', 'xxx']) result = runner.invoke(showtype, ["-f", "xxx"])
assert 'Error: Invalid value for "-f": Path "xxx" does not exist' \ assert "does not exist" in result.output
in result.output
result = runner.invoke(showtype, ['-f', '.']) result = runner.invoke(showtype, ["-f", "."])
assert 'is_file=False' in result.output assert "is_file=False" in result.output
assert 'is_dir=True' in result.output assert "is_dir=True" in result.output
@click.command() @click.command()
@click.option('-f', type=click.Path()) @click.option("-f", type=click.Path())
def exists(f): def exists(f):
click.echo('exists=%s' % os.path.exists(f)) click.echo("exists={}".format(os.path.exists(f)))
with runner.isolated_filesystem(): with runner.isolated_filesystem():
result = runner.invoke(exists, ['-f', 'xxx']) result = runner.invoke(exists, ["-f", "xxx"])
assert 'exists=False' in result.output assert "exists=False" in result.output
result = runner.invoke(exists, ['-f', '.']) result = runner.invoke(exists, ["-f", "."])
assert 'exists=True' in result.output assert "exists=True" in result.output
def test_choice_option(runner): def test_choice_option(runner):
@click.command() @click.command()
@click.option('--method', type=click.Choice(['foo', 'bar', 'baz'])) @click.option("--method", type=click.Choice(["foo", "bar", "baz"]))
def cli(method): def cli(method):
click.echo(method) click.echo(method)
result = runner.invoke(cli, ['--method=foo']) result = runner.invoke(cli, ["--method=foo"])
assert not result.exception assert not result.exception
assert result.output == 'foo\n' assert result.output == "foo\n"
result = runner.invoke(cli, ['--method=meh']) result = runner.invoke(cli, ["--method=meh"])
assert result.exit_code == 2 assert result.exit_code == 2
assert 'Invalid value for "--method": invalid choice: meh. ' \ assert (
'(choose from foo, bar, baz)' in result.output "Invalid value for '--method': invalid choice: meh."
" (choose from foo, bar, baz)" in result.output
)
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert '--method [foo|bar|baz]' in result.output assert "--method [foo|bar|baz]" in result.output
def test_datetime_option_default(runner): def test_datetime_option_default(runner):
@click.command() @click.command()
@click.option('--start_date', type=click.DateTime()) @click.option("--start_date", type=click.DateTime())
def cli(start_date): def cli(start_date):
click.echo(start_date.strftime('%Y-%m-%dT%H:%M:%S')) click.echo(start_date.strftime("%Y-%m-%dT%H:%M:%S"))
result = runner.invoke(cli, ['--start_date=2015-09-29']) result = runner.invoke(cli, ["--start_date=2015-09-29"])
assert not result.exception assert not result.exception
assert result.output == '2015-09-29T00:00:00\n' assert result.output == "2015-09-29T00:00:00\n"
result = runner.invoke(cli, ['--start_date=2015-09-29T09:11:22']) result = runner.invoke(cli, ["--start_date=2015-09-29T09:11:22"])
assert not result.exception assert not result.exception
assert result.output == '2015-09-29T09:11:22\n' assert result.output == "2015-09-29T09:11:22\n"
result = runner.invoke(cli, ['--start_date=2015-09']) result = runner.invoke(cli, ["--start_date=2015-09"])
assert result.exit_code == 2 assert result.exit_code == 2
assert ('Invalid value for "--start_date": ' assert (
'invalid datetime format: 2015-09. ' "Invalid value for '--start_date':"
'(choose from %Y-%m-%d, %Y-%m-%dT%H:%M:%S, %Y-%m-%d %H:%M:%S)' " invalid datetime format: 2015-09."
) in result.output " (choose from %Y-%m-%d, %Y-%m-%dT%H:%M:%S, %Y-%m-%d %H:%M:%S)"
) in result.output
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert '--start_date [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]' in result.output assert (
"--start_date [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]" in result.output
)
def test_datetime_option_custom(runner): def test_datetime_option_custom(runner):
@click.command() @click.command()
@click.option('--start_date', @click.option("--start_date", type=click.DateTime(formats=["%A %B %d, %Y"]))
type=click.DateTime(formats=['%A %B %d, %Y']))
def cli(start_date): def cli(start_date):
click.echo(start_date.strftime('%Y-%m-%dT%H:%M:%S')) click.echo(start_date.strftime("%Y-%m-%dT%H:%M:%S"))
result = runner.invoke(cli, ['--start_date=Wednesday June 05, 2010']) result = runner.invoke(cli, ["--start_date=Wednesday June 05, 2010"])
assert not result.exception assert not result.exception
assert result.output == '2010-06-05T00:00:00\n' assert result.output == "2010-06-05T00:00:00\n"
def test_int_range_option(runner): def test_int_range_option(runner):
@click.command() @click.command()
@click.option('--x', type=click.IntRange(0, 5)) @click.option("--x", type=click.IntRange(0, 5))
def cli(x): def cli(x):
click.echo(x) click.echo(x)
result = runner.invoke(cli, ['--x=5']) result = runner.invoke(cli, ["--x=5"])
assert not result.exception assert not result.exception
assert result.output == '5\n' assert result.output == "5\n"
result = runner.invoke(cli, ['--x=6']) result = runner.invoke(cli, ["--x=6"])
assert result.exit_code == 2 assert result.exit_code == 2
assert 'Invalid value for "--x": 6 is not in the valid range of 0 to 5.\n' \ assert (
"Invalid value for '--x': 6 is not in the valid range of 0 to 5.\n"
in result.output in result.output
)
@click.command() @click.command()
@click.option('--x', type=click.IntRange(0, 5, clamp=True)) @click.option("--x", type=click.IntRange(0, 5, clamp=True))
def clamp(x): def clamp(x):
click.echo(x) click.echo(x)
result = runner.invoke(clamp, ['--x=5']) result = runner.invoke(clamp, ["--x=5"])
assert not result.exception assert not result.exception
assert result.output == '5\n' assert result.output == "5\n"
result = runner.invoke(clamp, ['--x=6']) result = runner.invoke(clamp, ["--x=6"])
assert not result.exception assert not result.exception
assert result.output == '5\n' assert result.output == "5\n"
result = runner.invoke(clamp, ['--x=-1']) result = runner.invoke(clamp, ["--x=-1"])
assert not result.exception assert not result.exception
assert result.output == '0\n' assert result.output == "0\n"
def test_float_range_option(runner): def test_float_range_option(runner):
@click.command() @click.command()
@click.option('--x', type=click.FloatRange(0, 5)) @click.option("--x", type=click.FloatRange(0, 5))
def cli(x): def cli(x):
click.echo(x) click.echo(x)
result = runner.invoke(cli, ['--x=5.0']) result = runner.invoke(cli, ["--x=5.0"])
assert not result.exception assert not result.exception
assert result.output == '5.0\n' assert result.output == "5.0\n"
result = runner.invoke(cli, ['--x=6.0']) result = runner.invoke(cli, ["--x=6.0"])
assert result.exit_code == 2 assert result.exit_code == 2
assert 'Invalid value for "--x": 6.0 is not in the valid range of 0 to 5.\n' \ assert (
"Invalid value for '--x': 6.0 is not in the valid range of 0 to 5.\n"
in result.output in result.output
)
@click.command() @click.command()
@click.option('--x', type=click.FloatRange(0, 5, clamp=True)) @click.option("--x", type=click.FloatRange(0, 5, clamp=True))
def clamp(x): def clamp(x):
click.echo(x) click.echo(x)
result = runner.invoke(clamp, ['--x=5.0']) result = runner.invoke(clamp, ["--x=5.0"])
assert not result.exception assert not result.exception
assert result.output == '5.0\n' assert result.output == "5.0\n"
result = runner.invoke(clamp, ['--x=6.0']) result = runner.invoke(clamp, ["--x=6.0"])
assert not result.exception assert not result.exception
assert result.output == '5\n' assert result.output == "5\n"
result = runner.invoke(clamp, ['--x=-1.0']) result = runner.invoke(clamp, ["--x=-1.0"])
assert not result.exception assert not result.exception
assert result.output == '0\n' assert result.output == "0\n"
def test_required_option(runner): def test_required_option(runner):
@click.command() @click.command()
@click.option('--foo', required=True) @click.option("--foo", required=True)
def cli(foo): def cli(foo):
click.echo(foo) click.echo(foo)
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.exit_code == 2 assert result.exit_code == 2
assert 'Missing option "--foo"' in result.output assert "Missing option '--foo'" in result.output
def test_evaluation_order(runner): def test_evaluation_order(runner):
@ -455,52 +481,50 @@ def test_evaluation_order(runner):
return value return value
@click.command() @click.command()
@click.option('--missing', default='missing', @click.option("--missing", default="missing", is_eager=False, callback=memo)
is_eager=False, callback=memo) @click.option("--eager-flag1", flag_value="eager1", is_eager=True, callback=memo)
@click.option('--eager-flag1', flag_value='eager1', @click.option("--eager-flag2", flag_value="eager2", is_eager=True, callback=memo)
is_eager=True, callback=memo) @click.option("--eager-flag3", flag_value="eager3", is_eager=True, callback=memo)
@click.option('--eager-flag2', flag_value='eager2', @click.option("--normal-flag1", flag_value="normal1", is_eager=False, callback=memo)
is_eager=True, callback=memo) @click.option("--normal-flag2", flag_value="normal2", is_eager=False, callback=memo)
@click.option('--eager-flag3', flag_value='eager3', @click.option("--normal-flag3", flag_value="normal3", is_eager=False, callback=memo)
is_eager=True, callback=memo)
@click.option('--normal-flag1', flag_value='normal1',
is_eager=False, callback=memo)
@click.option('--normal-flag2', flag_value='normal2',
is_eager=False, callback=memo)
@click.option('--normal-flag3', flag_value='normal3',
is_eager=False, callback=memo)
def cli(**x): def cli(**x):
pass pass
result = runner.invoke(cli, ['--eager-flag2', result = runner.invoke(
'--eager-flag1', cli,
'--normal-flag2', [
'--eager-flag3', "--eager-flag2",
'--normal-flag3', "--eager-flag1",
'--normal-flag3', "--normal-flag2",
'--normal-flag1', "--eager-flag3",
'--normal-flag1']) "--normal-flag3",
"--normal-flag3",
"--normal-flag1",
"--normal-flag1",
],
)
assert not result.exception assert not result.exception
assert called == [ assert called == [
'eager2', "eager2",
'eager1', "eager1",
'eager3', "eager3",
'normal2', "normal2",
'normal3', "normal3",
'normal1', "normal1",
'missing', "missing",
] ]
def test_hidden_option(runner): def test_hidden_option(runner):
@click.command() @click.command()
@click.option('--nope', hidden=True) @click.option("--nope", hidden=True)
def cli(nope): def cli(nope):
click.echo(nope) click.echo(nope)
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert '--nope' not in result.output assert "--nope" not in result.output
def test_hidden_command(runner): def test_hidden_command(runner):
@ -512,9 +536,9 @@ def test_hidden_command(runner):
def nope(): def nope():
pass pass
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert 'nope' not in result.output assert "nope" not in result.output
def test_hidden_group(runner): def test_hidden_group(runner):
@ -530,7 +554,7 @@ def test_hidden_group(runner):
def nope(): def nope():
pass pass
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert 'subgroup' not in result.output assert "subgroup" not in result.output
assert 'nope' not in result.output assert "nope" not in result.output

View file

@ -1,13 +1,16 @@
import sys import sys
import click
import pytest import pytest
import click
def debug(): def debug():
click.echo('%s=%s' % ( click.echo(
sys._getframe(1).f_code.co_name, "{}={}".format(
'|'.join(click.get_current_context().args), sys._getframe(1).f_code.co_name, "|".join(click.get_current_context().args)
)) )
)
def test_basic_chaining(runner): def test_basic_chaining(runner):
@ -15,20 +18,20 @@ def test_basic_chaining(runner):
def cli(): def cli():
pass pass
@cli.command('sdist') @cli.command("sdist")
def sdist(): def sdist():
click.echo('sdist called') click.echo("sdist called")
@cli.command('bdist') @cli.command("bdist")
def bdist(): def bdist():
click.echo('bdist called') click.echo("bdist called")
result = runner.invoke(cli, ['bdist', 'sdist', 'bdist']) result = runner.invoke(cli, ["bdist", "sdist", "bdist"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'bdist called', "bdist called",
'sdist called', "sdist called",
'bdist called', "bdist called",
] ]
@ -38,32 +41,32 @@ def test_chaining_help(runner):
"""ROOT HELP""" """ROOT HELP"""
pass pass
@cli.command('sdist') @cli.command("sdist")
def sdist(): def sdist():
"""SDIST HELP""" """SDIST HELP"""
click.echo('sdist called') click.echo("sdist called")
@cli.command('bdist') @cli.command("bdist")
def bdist(): def bdist():
"""BDIST HELP""" """BDIST HELP"""
click.echo('bdist called') click.echo("bdist called")
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert not result.exception assert not result.exception
assert 'COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...' in result.output assert "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." in result.output
assert 'ROOT HELP' in result.output assert "ROOT HELP" in result.output
result = runner.invoke(cli, ['sdist', '--help']) result = runner.invoke(cli, ["sdist", "--help"])
assert not result.exception assert not result.exception
assert 'SDIST HELP' in result.output assert "SDIST HELP" in result.output
result = runner.invoke(cli, ['bdist', '--help']) result = runner.invoke(cli, ["bdist", "--help"])
assert not result.exception assert not result.exception
assert 'BDIST HELP' in result.output assert "BDIST HELP" in result.output
result = runner.invoke(cli, ['bdist', 'sdist', '--help']) result = runner.invoke(cli, ["bdist", "sdist", "--help"])
assert not result.exception assert not result.exception
assert 'SDIST HELP' in result.output assert "SDIST HELP" in result.output
def test_chaining_with_options(runner): def test_chaining_with_options(runner):
@ -71,22 +74,19 @@ def test_chaining_with_options(runner):
def cli(): def cli():
pass pass
@cli.command('sdist') @cli.command("sdist")
@click.option('--format') @click.option("--format")
def sdist(format): def sdist(format):
click.echo('sdist called %s' % format) click.echo("sdist called {}".format(format))
@cli.command('bdist') @cli.command("bdist")
@click.option('--format') @click.option("--format")
def bdist(format): def bdist(format):
click.echo('bdist called %s' % format) click.echo("bdist called {}".format(format))
result = runner.invoke(cli, ['bdist', '--format=1', 'sdist', '--format=2']) result = runner.invoke(cli, ["bdist", "--format=1", "sdist", "--format=2"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]
'bdist called 1',
'sdist called 2',
]
def test_chaining_with_arguments(runner): def test_chaining_with_arguments(runner):
@ -94,73 +94,62 @@ def test_chaining_with_arguments(runner):
def cli(): def cli():
pass pass
@cli.command('sdist') @cli.command("sdist")
@click.argument('format') @click.argument("format")
def sdist(format): def sdist(format):
click.echo('sdist called %s' % format) click.echo("sdist called {}".format(format))
@cli.command('bdist') @cli.command("bdist")
@click.argument('format') @click.argument("format")
def bdist(format): def bdist(format):
click.echo('bdist called %s' % format) click.echo("bdist called {}".format(format))
result = runner.invoke(cli, ['bdist', '1', 'sdist', '2']) result = runner.invoke(cli, ["bdist", "1", "sdist", "2"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]
'bdist called 1',
'sdist called 2',
]
def test_pipeline(runner): def test_pipeline(runner):
@click.group(chain=True, invoke_without_command=True) @click.group(chain=True, invoke_without_command=True)
@click.option('-i', '--input', type=click.File('r')) @click.option("-i", "--input", type=click.File("r"))
def cli(input): def cli(input):
pass pass
@cli.resultcallback() @cli.resultcallback()
def process_pipeline(processors, input): def process_pipeline(processors, input):
iterator = (x.rstrip('\r\n') for x in input) iterator = (x.rstrip("\r\n") for x in input)
for processor in processors: for processor in processors:
iterator = processor(iterator) iterator = processor(iterator)
for item in iterator: for item in iterator:
click.echo(item) click.echo(item)
@cli.command('uppercase') @cli.command("uppercase")
def make_uppercase(): def make_uppercase():
def processor(iterator): def processor(iterator):
for line in iterator: for line in iterator:
yield line.upper() yield line.upper()
return processor return processor
@cli.command('strip') @cli.command("strip")
def make_strip(): def make_strip():
def processor(iterator): def processor(iterator):
for line in iterator: for line in iterator:
yield line.strip() yield line.strip()
return processor return processor
result = runner.invoke(cli, ['-i', '-'], input='foo\nbar') result = runner.invoke(cli, ["-i", "-"], input="foo\nbar")
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["foo", "bar"]
'foo',
'bar',
]
result = runner.invoke(cli, ['-i', '-', 'strip'], input='foo \n bar') result = runner.invoke(cli, ["-i", "-", "strip"], input="foo \n bar")
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["foo", "bar"]
'foo',
'bar',
]
result = runner.invoke(cli, ['-i', '-', 'strip', 'uppercase'], result = runner.invoke(cli, ["-i", "-", "strip", "uppercase"], input="foo \n bar")
input='foo \n bar')
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["FOO", "BAR"]
'FOO',
'BAR',
]
def test_args_and_chain(runner): def test_args_and_chain(runner):
@ -180,44 +169,38 @@ def test_args_and_chain(runner):
def c(): def c():
debug() debug()
result = runner.invoke(cli, ['a', 'b', 'c']) result = runner.invoke(cli, ["a", "b", "c"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["cli=", "a=", "b=", "c="]
'cli=',
'a=',
'b=',
'c=',
]
def test_multicommand_arg_behavior(runner): def test_multicommand_arg_behavior(runner):
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
@click.group(chain=True) @click.group(chain=True)
@click.argument('forbidden', required=False) @click.argument("forbidden", required=False)
def bad_cli(): def bad_cli():
pass pass
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
@click.group(chain=True) @click.group(chain=True)
@click.argument('forbidden', nargs=-1) @click.argument("forbidden", nargs=-1)
def bad_cli2(): def bad_cli2():
pass pass
@click.group(chain=True) @click.group(chain=True)
@click.argument('arg') @click.argument("arg")
def cli(arg): def cli(arg):
click.echo('cli:%s' % arg) click.echo("cli:{}".format(arg))
@cli.command() @cli.command()
def a(): def a():
click.echo('a') click.echo("a")
result = runner.invoke(cli, ['foo', 'a']) result = runner.invoke(cli, ["foo", "a"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["cli:foo", "a"]
'cli:foo',
'a',
]
@pytest.mark.xfail @pytest.mark.xfail
@ -242,11 +225,6 @@ def test_multicommand_chaining(runner):
def l1b(): def l1b():
debug() debug()
result = runner.invoke(cli, ['l1a', 'l2a', 'l1b']) result = runner.invoke(cli, ["l1a", "l2a", "l1b"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["cli=", "l1a=", "l2a=", "l1b="]
'cli=',
'l1a=',
'l2a=',
'l1b=',
]

View file

@ -2,7 +2,6 @@
import re import re
import click import click
import pytest
def test_other_command_invoke(runner): def test_other_command_invoke(runner):
@ -12,33 +11,33 @@ def test_other_command_invoke(runner):
return ctx.invoke(other_cmd, arg=42) return ctx.invoke(other_cmd, arg=42)
@click.command() @click.command()
@click.argument('arg', type=click.INT) @click.argument("arg", type=click.INT)
def other_cmd(arg): def other_cmd(arg):
click.echo(arg) click.echo(arg)
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == '42\n' assert result.output == "42\n"
def test_other_command_forward(runner): def test_other_command_forward(runner):
cli = click.Group() cli = click.Group()
@cli.command() @cli.command()
@click.option('--count', default=1) @click.option("--count", default=1)
def test(count): def test(count):
click.echo('Count: %d' % count) click.echo("Count: {:d}".format(count))
@cli.command() @cli.command()
@click.option('--count', default=1) @click.option("--count", default=1)
@click.pass_context @click.pass_context
def dist(ctx, count): def dist(ctx, count):
ctx.forward(test) ctx.forward(test)
ctx.invoke(test, count=42) ctx.invoke(test, count=42)
result = runner.invoke(cli, ['dist']) result = runner.invoke(cli, ["dist"])
assert not result.exception assert not result.exception
assert result.output == 'Count: 1\nCount: 42\n' assert result.output == "Count: 1\nCount: 42\n"
def test_auto_shorthelp(runner): def test_auto_shorthelp(runner):
@ -59,13 +58,28 @@ def test_auto_shorthelp(runner):
"""This is a long text that is too long to show as short help """This is a long text that is too long to show as short help
and will be truncated instead.""" and will be truncated instead."""
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert re.search( assert (
r'Commands:\n\s+' re.search(
r'long\s+This is a long text that is too long to show as short help\.\.\.\n\s+' r"Commands:\n\s+"
r'short\s+This is a short text\.\n\s+' r"long\s+This is a long text that is too long to show as short help"
r'special-chars\s+Login and store the token in ~/.netrc\.\s*', r"\.\.\.\n\s+"
result.output) is not None r"short\s+This is a short text\.\n\s+"
r"special-chars\s+Login and store the token in ~/.netrc\.\s*",
result.output,
)
is not None
)
def test_no_args_is_help(runner):
@click.command(no_args_is_help=True)
def cli():
pass
result = runner.invoke(cli, [])
assert result.exit_code == 0
assert "Show this message and exit." in result.output
def test_default_maps(runner): def test_default_maps(runner):
@ -74,43 +88,41 @@ def test_default_maps(runner):
pass pass
@cli.command() @cli.command()
@click.option('--name', default='normal') @click.option("--name", default="normal")
def foo(name): def foo(name):
click.echo(name) click.echo(name)
result = runner.invoke(cli, ['foo'], default_map={ result = runner.invoke(cli, ["foo"], default_map={"foo": {"name": "changed"}})
'foo': {'name': 'changed'}
})
assert not result.exception assert not result.exception
assert result.output == 'changed\n' assert result.output == "changed\n"
def test_group_with_args(runner): def test_group_with_args(runner):
@click.group() @click.group()
@click.argument('obj') @click.argument("obj")
def cli(obj): def cli(obj):
click.echo('obj=%s' % obj) click.echo("obj={}".format(obj))
@cli.command() @cli.command()
def move(): def move():
click.echo('move') click.echo("move")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.exit_code == 0 assert result.exit_code == 0
assert 'Show this message and exit.' in result.output assert "Show this message and exit." in result.output
result = runner.invoke(cli, ['obj1']) result = runner.invoke(cli, ["obj1"])
assert result.exit_code == 2 assert result.exit_code == 2
assert 'Error: Missing command.' in result.output assert "Error: Missing command." in result.output
result = runner.invoke(cli, ['obj1', '--help']) result = runner.invoke(cli, ["obj1", "--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert 'Show this message and exit.' in result.output assert "Show this message and exit." in result.output
result = runner.invoke(cli, ['obj1', 'move']) result = runner.invoke(cli, ["obj1", "move"])
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'obj=obj1\nmove\n' assert result.output == "obj=obj1\nmove\n"
def test_base_command(runner): def test_base_command(runner):
@ -121,7 +133,6 @@ def test_base_command(runner):
pass pass
class OptParseCommand(click.BaseCommand): class OptParseCommand(click.BaseCommand):
def __init__(self, name, parser, callback): def __init__(self, name, parser, callback):
click.BaseCommand.__init__(self, name) click.BaseCommand.__init__(self, name)
self.parser = parser self.parser = parser
@ -144,58 +155,67 @@ def test_base_command(runner):
def invoke(self, ctx): def invoke(self, ctx):
ctx.invoke(self.callback, ctx.args, **ctx.params) ctx.invoke(self.callback, ctx.args, **ctx.params)
parser = optparse.OptionParser(usage='Usage: foo test [OPTIONS]') parser = optparse.OptionParser(usage="Usage: foo test [OPTIONS]")
parser.add_option("-f", "--file", dest="filename", parser.add_option(
help="write report to FILE", metavar="FILE") "-f", "--file", dest="filename", help="write report to FILE", metavar="FILE"
parser.add_option("-q", "--quiet", )
action="store_false", dest="verbose", default=True, parser.add_option(
help="don't print status messages to stdout") "-q",
"--quiet",
action="store_false",
dest="verbose",
default=True,
help="don't print status messages to stdout",
)
def test_callback(args, filename, verbose): def test_callback(args, filename, verbose):
click.echo(' '.join(args)) click.echo(" ".join(args))
click.echo(filename) click.echo(filename)
click.echo(verbose) click.echo(verbose)
cli.add_command(OptParseCommand('test', parser, test_callback))
result = runner.invoke(cli, ['test', '-f', 'test.txt', '-q', cli.add_command(OptParseCommand("test", parser, test_callback))
'whatever.txt', 'whateverelse.txt'])
result = runner.invoke(
cli, ["test", "-f", "test.txt", "-q", "whatever.txt", "whateverelse.txt"]
)
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'whatever.txt whateverelse.txt', "whatever.txt whateverelse.txt",
'test.txt', "test.txt",
'False', "False",
] ]
result = runner.invoke(cli, ['test', '--help']) result = runner.invoke(cli, ["test", "--help"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: foo test [OPTIONS]', "Usage: foo test [OPTIONS]",
'', "",
'Options:', "Options:",
' -h, --help show this help message and exit', " -h, --help show this help message and exit",
' -f FILE, --file=FILE write report to FILE', " -f FILE, --file=FILE write report to FILE",
' -q, --quiet don\'t print status messages to stdout', " -q, --quiet don't print status messages to stdout",
] ]
def test_object_propagation(runner): def test_object_propagation(runner):
for chain in False, True: for chain in False, True:
@click.group(chain=chain) @click.group(chain=chain)
@click.option('--debug/--no-debug', default=False) @click.option("--debug/--no-debug", default=False)
@click.pass_context @click.pass_context
def cli(ctx, debug): def cli(ctx, debug):
if ctx.obj is None: if ctx.obj is None:
ctx.obj = {} ctx.obj = {}
ctx.obj['DEBUG'] = debug ctx.obj["DEBUG"] = debug
@cli.command() @cli.command()
@click.pass_context @click.pass_context
def sync(ctx): def sync(ctx):
click.echo('Debug is %s' % (ctx.obj['DEBUG'] and 'on' or 'off')) click.echo("Debug is {}".format("on" if ctx.obj["DEBUG"] else "off"))
result = runner.invoke(cli, ['sync']) result = runner.invoke(cli, ["sync"])
assert result.exception is None assert result.exception is None
assert result.output == 'Debug is off\n' assert result.output == "Debug is off\n"
def test_other_command_invoke_with_defaults(runner): def test_other_command_invoke_with_defaults(runner):
@ -205,15 +225,15 @@ def test_other_command_invoke_with_defaults(runner):
return ctx.invoke(other_cmd) return ctx.invoke(other_cmd)
@click.command() @click.command()
@click.option('--foo', type=click.INT, default=42) @click.option("--foo", type=click.INT, default=42)
@click.pass_context @click.pass_context
def other_cmd(ctx, foo): def other_cmd(ctx, foo):
assert ctx.info_name == 'other-cmd' assert ctx.info_name == "other-cmd"
click.echo(foo) click.echo(foo)
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == '42\n' assert result.output == "42\n"
def test_invoked_subcommand(runner): def test_invoked_subcommand(runner):
@ -221,39 +241,37 @@ def test_invoked_subcommand(runner):
@click.pass_context @click.pass_context
def cli(ctx): def cli(ctx):
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
click.echo('no subcommand, use default') click.echo("no subcommand, use default")
ctx.invoke(sync) ctx.invoke(sync)
else: else:
click.echo('invoke subcommand') click.echo("invoke subcommand")
@cli.command() @cli.command()
def sync(): def sync():
click.echo('in subcommand') click.echo("in subcommand")
result = runner.invoke(cli, ['sync']) result = runner.invoke(cli, ["sync"])
assert not result.exception assert not result.exception
assert result.output == 'invoke subcommand\nin subcommand\n' assert result.output == "invoke subcommand\nin subcommand\n"
result = runner.invoke(cli) result = runner.invoke(cli)
assert not result.exception assert not result.exception
assert result.output == 'no subcommand, use default\nin subcommand\n' assert result.output == "no subcommand, use default\nin subcommand\n"
def test_unprocessed_options(runner): def test_unprocessed_options(runner):
@click.command(context_settings=dict( @click.command(context_settings=dict(ignore_unknown_options=True))
ignore_unknown_options=True @click.argument("args", nargs=-1, type=click.UNPROCESSED)
)) @click.option("--verbose", "-v", count=True)
@click.argument('args', nargs=-1, type=click.UNPROCESSED)
@click.option('--verbose', '-v', count=True)
def cli(verbose, args): def cli(verbose, args):
click.echo('Verbosity: %s' % verbose) click.echo("Verbosity: {}".format(verbose))
click.echo('Args: %s' % '|'.join(args)) click.echo("Args: {}".format("|".join(args)))
result = runner.invoke(cli, ['-foo', '-vvvvx', '--muhaha', 'x', 'y', '-x']) result = runner.invoke(cli, ["-foo", "-vvvvx", "--muhaha", "x", "y", "-x"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Verbosity: 4', "Verbosity: 4",
'Args: -foo|-x|--muhaha|x|y|-x', "Args: -foo|-x|--muhaha|x|y|-x",
] ]
@ -263,21 +281,21 @@ def test_deprecated_in_help_messages(runner):
"""CLI HELP""" """CLI HELP"""
pass pass
result = runner.invoke(cmd_with_help, ['--help']) result = runner.invoke(cmd_with_help, ["--help"])
assert '(DEPRECATED)' in result.output assert "(DEPRECATED)" in result.output
@click.command(deprecated=True) @click.command(deprecated=True)
def cmd_without_help(): def cmd_without_help():
pass pass
result = runner.invoke(cmd_without_help, ['--help']) result = runner.invoke(cmd_without_help, ["--help"])
assert '(DEPRECATED)' in result.output assert "(DEPRECATED)" in result.output
def test_deprecated_in_invocation(runner): def test_deprecated_in_invocation(runner):
@click.command(deprecated=True) @click.command(deprecated=True)
def deprecated_cmd(): def deprecated_cmd():
debug() pass
result = runner.invoke(deprecated_cmd) result = runner.invoke(deprecated_cmd)
assert 'DeprecationWarning:' in result.output assert "DeprecationWarning:" in result.output

View file

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

View file

@ -5,7 +5,7 @@ import click
def test_ensure_context_objects(runner): def test_ensure_context_objects(runner):
class Foo(object): class Foo(object):
def __init__(self): def __init__(self):
self.title = 'default' self.title = "default"
pass_foo = click.make_pass_decorator(Foo, ensure=True) pass_foo = click.make_pass_decorator(Foo, ensure=True)
@ -19,15 +19,15 @@ def test_ensure_context_objects(runner):
def test(foo): def test(foo):
click.echo(foo.title) click.echo(foo.title)
result = runner.invoke(cli, ['test']) result = runner.invoke(cli, ["test"])
assert not result.exception assert not result.exception
assert result.output == 'default\n' assert result.output == "default\n"
def test_get_context_objects(runner): def test_get_context_objects(runner):
class Foo(object): class Foo(object):
def __init__(self): def __init__(self):
self.title = 'default' self.title = "default"
pass_foo = click.make_pass_decorator(Foo, ensure=True) pass_foo = click.make_pass_decorator(Foo, ensure=True)
@ -35,22 +35,22 @@ def test_get_context_objects(runner):
@click.pass_context @click.pass_context
def cli(ctx): def cli(ctx):
ctx.obj = Foo() ctx.obj = Foo()
ctx.obj.title = 'test' ctx.obj.title = "test"
@cli.command() @cli.command()
@pass_foo @pass_foo
def test(foo): def test(foo):
click.echo(foo.title) click.echo(foo.title)
result = runner.invoke(cli, ['test']) result = runner.invoke(cli, ["test"])
assert not result.exception assert not result.exception
assert result.output == 'test\n' assert result.output == "test\n"
def test_get_context_objects_no_ensuring(runner): def test_get_context_objects_no_ensuring(runner):
class Foo(object): class Foo(object):
def __init__(self): def __init__(self):
self.title = 'default' self.title = "default"
pass_foo = click.make_pass_decorator(Foo) pass_foo = click.make_pass_decorator(Foo)
@ -58,16 +58,16 @@ def test_get_context_objects_no_ensuring(runner):
@click.pass_context @click.pass_context
def cli(ctx): def cli(ctx):
ctx.obj = Foo() ctx.obj = Foo()
ctx.obj.title = 'test' ctx.obj.title = "test"
@cli.command() @cli.command()
@pass_foo @pass_foo
def test(foo): def test(foo):
click.echo(foo.title) click.echo(foo.title)
result = runner.invoke(cli, ['test']) result = runner.invoke(cli, ["test"])
assert not result.exception assert not result.exception
assert result.output == 'test\n' assert result.output == "test\n"
def test_get_context_objects_missing(runner): def test_get_context_objects_missing(runner):
@ -86,11 +86,13 @@ def test_get_context_objects_missing(runner):
def test(foo): def test(foo):
click.echo(foo.title) click.echo(foo.title)
result = runner.invoke(cli, ['test']) result = runner.invoke(cli, ["test"])
assert result.exception is not None assert result.exception is not None
assert isinstance(result.exception, RuntimeError) assert isinstance(result.exception, RuntimeError)
assert "Managed to invoke callback without a context object " \ assert (
"of type 'Foo' existing" in str(result.exception) "Managed to invoke callback without a context object of type"
" 'Foo' existing" in str(result.exception)
)
def test_multi_enter(runner): def test_multi_enter(runner):
@ -101,6 +103,7 @@ def test_multi_enter(runner):
def cli(ctx): def cli(ctx):
def callback(): def callback():
called.append(True) called.append(True)
ctx.call_on_close(callback) ctx.call_on_close(callback)
with ctx: with ctx:
@ -117,8 +120,8 @@ def test_global_context_object(runner):
@click.pass_context @click.pass_context
def cli(ctx): def cli(ctx):
assert click.get_current_context() is ctx assert click.get_current_context() is ctx
ctx.obj = 'FOOBAR' ctx.obj = "FOOBAR"
assert click.get_current_context().obj == 'FOOBAR' assert click.get_current_context().obj == "FOOBAR"
assert click.get_current_context(silent=True) is None assert click.get_current_context(silent=True) is None
runner.invoke(cli, [], catch_exceptions=False) runner.invoke(cli, [], catch_exceptions=False)
@ -126,20 +129,20 @@ def test_global_context_object(runner):
def test_context_meta(runner): def test_context_meta(runner):
LANG_KEY = __name__ + '.lang' LANG_KEY = "{}.lang".format(__name__)
def set_language(value): def set_language(value):
click.get_current_context().meta[LANG_KEY] = value click.get_current_context().meta[LANG_KEY] = value
def get_language(): def get_language():
return click.get_current_context().meta.get(LANG_KEY, 'en_US') return click.get_current_context().meta.get(LANG_KEY, "en_US")
@click.command() @click.command()
@click.pass_context @click.pass_context
def cli(ctx): def cli(ctx):
assert get_language() == 'en_US' assert get_language() == "en_US"
set_language('de_DE') set_language("de_DE")
assert get_language() == 'de_DE' assert get_language() == "de_DE"
runner.invoke(cli, [], catch_exceptions=False) runner.invoke(cli, [], catch_exceptions=False)
@ -174,16 +177,16 @@ def test_pass_obj(runner):
@click.group() @click.group()
@click.pass_context @click.pass_context
def cli(ctx): def cli(ctx):
ctx.obj = 'test' ctx.obj = "test"
@cli.command() @cli.command()
@click.pass_obj @click.pass_obj
def test(obj): def test(obj):
click.echo(obj) click.echo(obj)
result = runner.invoke(cli, ['test']) result = runner.invoke(cli, ["test"])
assert not result.exception assert not result.exception
assert result.output == 'test\n' assert result.output == "test\n"
def test_close_before_pop(runner): def test_close_before_pop(runner):
@ -192,17 +195,18 @@ def test_close_before_pop(runner):
@click.command() @click.command()
@click.pass_context @click.pass_context
def cli(ctx): def cli(ctx):
ctx.obj = 'test' ctx.obj = "test"
@ctx.call_on_close @ctx.call_on_close
def foo(): def foo():
assert click.get_current_context().obj == 'test' assert click.get_current_context().obj == "test"
called.append(True) called.append(True)
click.echo('aha!')
click.echo("aha!")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == 'aha!\n' assert result.output == "aha!\n"
assert called == [True] assert called == [True]
@ -211,8 +215,9 @@ def test_make_pass_decorator_args(runner):
Test to check that make_pass_decorator doesn't consume arguments based on Test to check that make_pass_decorator doesn't consume arguments based on
invocation order. invocation order.
""" """
class Foo(object): class Foo(object):
title = 'foocmd' title = "foocmd"
pass_foo = click.make_pass_decorator(Foo) pass_foo = click.make_pass_decorator(Foo)
@ -233,13 +238,13 @@ def test_make_pass_decorator_args(runner):
def test2(ctx, foo): def test2(ctx, foo):
click.echo(foo.title) click.echo(foo.title)
result = runner.invoke(cli, ['test1']) result = runner.invoke(cli, ["test1"])
assert not result.exception assert not result.exception
assert result.output == 'foocmd\n' assert result.output == "foocmd\n"
result = runner.invoke(cli, ['test2']) result = runner.invoke(cli, ["test2"])
assert not result.exception assert not result.exception
assert result.output == 'foocmd\n' assert result.output == "foocmd\n"
def test_exit_not_standalone(): def test_exit_not_standalone():
@ -248,11 +253,11 @@ def test_exit_not_standalone():
def cli(ctx): def cli(ctx):
ctx.exit(1) ctx.exit(1)
assert cli.main([], 'test_exit_not_standalone', standalone_mode=False) == 1 assert cli.main([], "test_exit_not_standalone", standalone_mode=False) == 1
@click.command() @click.command()
@click.pass_context @click.pass_context
def cli(ctx): def cli(ctx):
ctx.exit(0) ctx.exit(0)
assert cli.main([], 'test_exit_not_standalone', standalone_mode=False) == 0 assert cli.main([], "test_exit_not_standalone", standalone_mode=False) == 0

View file

@ -3,20 +3,19 @@ import click
def test_basic_defaults(runner): def test_basic_defaults(runner):
@click.command() @click.command()
@click.option('--foo', default=42, type=click.FLOAT) @click.option("--foo", default=42, type=click.FLOAT)
def cli(foo): def cli(foo):
assert type(foo) is float assert type(foo) is float
click.echo('FOO:[%s]' % foo) click.echo("FOO:[{}]".format(foo))
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert 'FOO:[42.0]' in result.output assert "FOO:[42.0]" in result.output
def test_multiple_defaults(runner): def test_multiple_defaults(runner):
@click.command() @click.command()
@click.option('--foo', default=[23, 42], type=click.FLOAT, @click.option("--foo", default=[23, 42], type=click.FLOAT, multiple=True)
multiple=True)
def cli(foo): def cli(foo):
for item in foo: for item in foo:
assert type(item) is float assert type(item) is float
@ -24,23 +23,18 @@ def test_multiple_defaults(runner):
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["23.0", "42.0"]
'23.0',
'42.0',
]
def test_nargs_plus_multiple(runner): def test_nargs_plus_multiple(runner):
@click.command() @click.command()
@click.option('--arg', default=((1, 2), (3, 4)), @click.option(
nargs=2, multiple=True, type=click.INT) "--arg", default=((1, 2), (3, 4)), nargs=2, multiple=True, type=click.INT
)
def cli(arg): def cli(arg):
for item in arg: for item in arg:
click.echo('<%d|%d>' % item) click.echo("<{0[0]:d}|{0[1]:d}>".format(item))
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["<1|2>", "<3|4>"]
'<1|2>',
'<3|4>',
]

View file

@ -25,28 +25,28 @@ def test_basic_functionality(runner):
that will be rewrapped again. that will be rewrapped again.
""" """
result = runner.invoke(cli, ['--help'], terminal_width=60) result = runner.invoke(cli, ["--help"], terminal_width=60)
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cli [OPTIONS]', "Usage: cli [OPTIONS]",
'', "",
' First paragraph.', " First paragraph.",
'', "",
' This is a very long second paragraph and not correctly', " This is a very long second paragraph and not correctly",
' wrapped but it will be rewrapped.', " wrapped but it will be rewrapped.",
'', "",
' This is', " This is",
' a paragraph', " a paragraph",
' without rewrapping.', " without rewrapping.",
'', "",
' 1', " 1",
' 2', " 2",
' 3', " 3",
'', "",
' And this is a paragraph that will be rewrapped again.', " And this is a paragraph that will be rewrapped again.",
'', "",
'Options:', "Options:",
' --help Show this message and exit.', " --help Show this message and exit.",
] ]
@ -62,30 +62,29 @@ def test_wrapping_long_options_strings(runner):
""" """
@a_very_long.command() @a_very_long.command()
@click.argument('first') @click.argument("first")
@click.argument('second') @click.argument("second")
@click.argument('third') @click.argument("third")
@click.argument('fourth') @click.argument("fourth")
@click.argument('fifth') @click.argument("fifth")
@click.argument('sixth') @click.argument("sixth")
def command(): def command():
"""A command. """A command.
""" """
# 54 is chosen as a length where the second line is one character # 54 is chosen as a length where the second line is one character
# longer than the maximum length. # longer than the maximum length.
result = runner.invoke(cli, ['a-very-long', 'command', '--help'], result = runner.invoke(cli, ["a-very-long", "command", "--help"], terminal_width=54)
terminal_width=54)
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cli a-very-long command [OPTIONS] FIRST SECOND', "Usage: cli a-very-long command [OPTIONS] FIRST SECOND",
' THIRD FOURTH FIFTH', " THIRD FOURTH FIFTH",
' SIXTH', " SIXTH",
'', "",
' A command.', " A command.",
'', "",
'Options:', "Options:",
' --help Show this message and exit.', " --help Show this message and exit.",
] ]
@ -101,28 +100,29 @@ def test_wrapping_long_command_name(runner):
""" """
@a_very_very_very_long.command() @a_very_very_very_long.command()
@click.argument('first') @click.argument("first")
@click.argument('second') @click.argument("second")
@click.argument('third') @click.argument("third")
@click.argument('fourth') @click.argument("fourth")
@click.argument('fifth') @click.argument("fifth")
@click.argument('sixth') @click.argument("sixth")
def command(): def command():
"""A command. """A command.
""" """
result = runner.invoke(cli, ['a-very-very-very-long', 'command', '--help'], result = runner.invoke(
terminal_width=54) cli, ["a-very-very-very-long", "command", "--help"], terminal_width=54
)
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cli a-very-very-very-long command ', "Usage: cli a-very-very-very-long command ",
' [OPTIONS] FIRST SECOND THIRD FOURTH FIFTH', " [OPTIONS] FIRST SECOND THIRD FOURTH FIFTH",
' SIXTH', " SIXTH",
'', "",
' A command.', " A command.",
'', "",
'Options:', "Options:",
' --help Show this message and exit.', " --help Show this message and exit.",
] ]
@ -133,33 +133,33 @@ def test_formatting_empty_help_lines(runner):
""" """
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cli [OPTIONS]', "Usage: cli [OPTIONS]",
'', "",
' Top level command', " Top level command",
'', "",
'', "",
'', "",
'Options:', "Options:",
' --help Show this message and exit.', " --help Show this message and exit.",
] ]
def test_formatting_usage_error(runner): def test_formatting_usage_error(runner):
@click.command() @click.command()
@click.argument('arg') @click.argument("arg")
def cmd(arg): def cmd(arg):
click.echo('arg:' + arg) click.echo("arg:{}".format(arg))
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.exit_code == 2 assert result.exit_code == 2
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cmd [OPTIONS] ARG', "Usage: cmd [OPTIONS] ARG",
'Try "cmd --help" for help.', "Try 'cmd --help' for help.",
'', "",
'Error: Missing argument "ARG".' "Error: Missing argument 'ARG'.",
] ]
@ -168,34 +168,35 @@ def test_formatting_usage_error_metavar_missing_arg(runner):
:author: @r-m-n :author: @r-m-n
Including attribution to #612 Including attribution to #612
""" """
@click.command() @click.command()
@click.argument('arg', metavar='metavar') @click.argument("arg", metavar="metavar")
def cmd(arg): def cmd(arg):
pass pass
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.exit_code == 2 assert result.exit_code == 2
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cmd [OPTIONS] metavar', "Usage: cmd [OPTIONS] metavar",
'Try "cmd --help" for help.', "Try 'cmd --help' for help.",
'', "",
'Error: Missing argument "metavar".' "Error: Missing argument 'metavar'.",
] ]
def test_formatting_usage_error_metavar_bad_arg(runner): def test_formatting_usage_error_metavar_bad_arg(runner):
@click.command() @click.command()
@click.argument('arg', type=click.INT, metavar='metavar') @click.argument("arg", type=click.INT, metavar="metavar")
def cmd(arg): def cmd(arg):
pass pass
result = runner.invoke(cmd, ['3.14']) result = runner.invoke(cmd, ["3.14"])
assert result.exit_code == 2 assert result.exit_code == 2
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cmd [OPTIONS] metavar', "Usage: cmd [OPTIONS] metavar",
'Try "cmd --help" for help.', "Try 'cmd --help' for help.",
'', "",
'Error: Invalid value for "metavar": 3.14 is not a valid integer' "Error: Invalid value for 'metavar': 3.14 is not a valid integer",
] ]
@ -205,50 +206,51 @@ def test_formatting_usage_error_nested(runner):
pass pass
@cmd.command() @cmd.command()
@click.argument('bar') @click.argument("bar")
def foo(bar): def foo(bar):
click.echo('foo:' + bar) click.echo("foo:{}".format(bar))
result = runner.invoke(cmd, ['foo']) result = runner.invoke(cmd, ["foo"])
assert result.exit_code == 2 assert result.exit_code == 2
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cmd foo [OPTIONS] BAR', "Usage: cmd foo [OPTIONS] BAR",
'Try "cmd foo --help" for help.', "Try 'cmd foo --help' for help.",
'', "",
'Error: Missing argument "BAR".' "Error: Missing argument 'BAR'.",
] ]
def test_formatting_usage_error_no_help(runner): def test_formatting_usage_error_no_help(runner):
@click.command(add_help_option=False) @click.command(add_help_option=False)
@click.argument('arg') @click.argument("arg")
def cmd(arg): def cmd(arg):
click.echo('arg:' + arg) click.echo("arg:{}".format(arg))
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.exit_code == 2 assert result.exit_code == 2
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cmd [OPTIONS] ARG', "Usage: cmd [OPTIONS] ARG",
'', "",
'Error: Missing argument "ARG".' "Error: Missing argument 'ARG'.",
] ]
def test_formatting_usage_custom_help(runner): def test_formatting_usage_custom_help(runner):
@click.command(context_settings=dict(help_option_names=['--man'])) @click.command(context_settings=dict(help_option_names=["--man"]))
@click.argument('arg') @click.argument("arg")
def cmd(arg): def cmd(arg):
click.echo('arg:' + arg) click.echo("arg:{}".format(arg))
result = runner.invoke(cmd, []) result = runner.invoke(cmd, [])
assert result.exit_code == 2 assert result.exit_code == 2
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cmd [OPTIONS] ARG', "Usage: cmd [OPTIONS] ARG",
'Try "cmd --man" for help.', "Try 'cmd --man' for help.",
'', "",
'Error: Missing argument "ARG".' "Error: Missing argument 'ARG'.",
] ]
def test_formatting_custom_type_metavar(runner): def test_formatting_custom_type_metavar(runner):
class MyType(click.ParamType): class MyType(click.ParamType):
def get_metavar(self, param): def get_metavar(self, param):
@ -260,13 +262,13 @@ def test_formatting_custom_type_metavar(runner):
def cmd(param): def cmd(param):
pass pass
result = runner.invoke(cmd, '--help') result = runner.invoke(cmd, "--help")
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: foo [OPTIONS] MY_TYPE', "Usage: foo [OPTIONS] MY_TYPE",
'', "",
'Options:', "Options:",
' --help Show this message and exit.' " --help Show this message and exit.",
] ]
@ -284,16 +286,68 @@ def test_truncating_docstring(runner):
:param click.core.Context ctx: Click context. :param click.core.Context ctx: Click context.
""" """
result = runner.invoke(cli, ['--help'], terminal_width=60) result = runner.invoke(cli, ["--help"], terminal_width=60)
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == [
'Usage: cli [OPTIONS]', "Usage: cli [OPTIONS]",
'', "",
' First paragraph.', " First paragraph.",
'', "",
' This is a very long second paragraph and not correctly', " This is a very long second paragraph and not correctly",
' wrapped but it will be rewrapped.', " wrapped but it will be rewrapped.",
'', "",
'Options:', "Options:",
' --help Show this message and exit.', " --help Show this message and exit.",
]
def test_global_show_default(runner):
@click.command(context_settings=dict(show_default=True))
@click.option("-f", "in_file", default="out.txt", help="Output file name")
def cli():
pass
result = runner.invoke(cli, ["--help"],)
assert result.output.splitlines() == [
"Usage: cli [OPTIONS]",
"",
"Options:",
" -f TEXT Output file name [default: out.txt]",
" --help Show this message and exit. [default: False]",
]
def test_formatting_usage_multiline_option_padding(runner):
@click.command("foo")
@click.option("--bar", help="This help message will be padded if it wraps.")
def cli():
pass
result = runner.invoke(cli, "--help", terminal_width=45)
assert not result.exception
assert result.output.splitlines() == [
"Usage: foo [OPTIONS]",
"",
"Options:",
" --bar TEXT This help message will be",
" padded if it wraps.",
"",
" --help Show this message and exit.",
]
def test_formatting_usage_no_option_padding(runner):
@click.command("foo")
@click.option("--bar", help="This help message will be padded if it wraps.")
def cli():
pass
result = runner.invoke(cli, "--help", terminal_width=80)
assert not result.exception
assert result.output.splitlines() == [
"Usage: foo [OPTIONS]",
"",
"Options:",
" --bar TEXT This help message will be padded if it wraps.",
" --help Show this message and exit.",
] ]

View file

@ -1,11 +1,11 @@
import sys
import json import json
import subprocess import subprocess
import sys
from click._compat import WIN from click._compat import WIN
IMPORT_TEST = b'''\ IMPORT_TEST = b"""\
try: try:
import __builtin__ as builtins import __builtin__ as builtins
except ImportError: except ImportError:
@ -27,29 +27,45 @@ import click
rv = list(found_imports) rv = list(found_imports)
import json import json
click.echo(json.dumps(rv)) click.echo(json.dumps(rv))
''' """
ALLOWED_IMPORTS = set([ ALLOWED_IMPORTS = {
'weakref', 'os', 'struct', 'collections', 'sys', 'contextlib', "weakref",
'functools', 'stat', 're', 'codecs', 'inspect', 'itertools', 'io', "os",
'threading', 'colorama', 'errno', 'fcntl', 'datetime' "struct",
]) "collections",
"sys",
"contextlib",
"functools",
"stat",
"re",
"codecs",
"inspect",
"itertools",
"io",
"threading",
"colorama",
"errno",
"fcntl",
"datetime",
"pipes",
}
if WIN: if WIN:
ALLOWED_IMPORTS.update(['ctypes', 'ctypes.wintypes', 'msvcrt', 'time', ALLOWED_IMPORTS.update(["ctypes", "ctypes.wintypes", "msvcrt", "time", "zlib"])
'zlib'])
def test_light_imports(): def test_light_imports():
c = subprocess.Popen([sys.executable, '-'], stdin=subprocess.PIPE, c = subprocess.Popen(
stdout=subprocess.PIPE) [sys.executable, "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
rv = c.communicate(IMPORT_TEST)[0] rv = c.communicate(IMPORT_TEST)[0]
if sys.version_info[0] != 2: if sys.version_info[0] != 2:
rv = rv.decode('utf-8') rv = rv.decode("utf-8")
imported = json.loads(rv) imported = json.loads(rv)
for module in imported: for module in imported:
if module == 'click' or module.startswith('click.'): if module == "click" or module.startswith("click."):
continue continue
assert module in ALLOWED_IMPORTS assert module in ALLOWED_IMPORTS

View file

@ -6,24 +6,24 @@ CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower())
def test_option_normalization(runner): def test_option_normalization(runner):
@click.command(context_settings=CONTEXT_SETTINGS) @click.command(context_settings=CONTEXT_SETTINGS)
@click.option('--foo') @click.option("--foo")
@click.option('-x') @click.option("-x")
def cli(foo, x): def cli(foo, x):
click.echo(foo) click.echo(foo)
click.echo(x) click.echo(x)
result = runner.invoke(cli, ['--FOO', '42', '-X', 23]) result = runner.invoke(cli, ["--FOO", "42", "-X", 23])
assert result.output == '42\n23\n' assert result.output == "42\n23\n"
def test_choice_normalization(runner): def test_choice_normalization(runner):
@click.command(context_settings=CONTEXT_SETTINGS) @click.command(context_settings=CONTEXT_SETTINGS)
@click.option('--choice', type=click.Choice(['Foo', 'Bar'])) @click.option("--choice", type=click.Choice(["Foo", "Bar"]))
def cli(choice): def cli(choice):
click.echo('Foo') click.echo(choice)
result = runner.invoke(cli, ['--CHOICE', 'FOO']) result = runner.invoke(cli, ["--CHOICE", "FOO"])
assert result.output == 'Foo\n' assert result.output == "Foo\n"
def test_command_normalization(runner): def test_command_normalization(runner):
@ -33,7 +33,7 @@ def test_command_normalization(runner):
@cli.command() @cli.command()
def foo(): def foo():
click.echo('here!') click.echo("here!")
result = runner.invoke(cli, ['FOO']) result = runner.invoke(cli, ["FOO"])
assert result.output == 'here!\n' assert result.output == "here!\n"

View file

@ -1,93 +1,85 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re
import os import os
import click import re
import pytest import pytest
import click
from click._compat import text_type from click._compat import text_type
def test_prefixes(runner): def test_prefixes(runner):
@click.command() @click.command()
@click.option('++foo', is_flag=True, help='das foo') @click.option("++foo", is_flag=True, help="das foo")
@click.option('--bar', is_flag=True, help='das bar') @click.option("--bar", is_flag=True, help="das bar")
def cli(foo, bar): def cli(foo, bar):
click.echo('foo=%s bar=%s' % (foo, bar)) click.echo("foo={} bar={}".format(foo, bar))
result = runner.invoke(cli, ['++foo', '--bar']) result = runner.invoke(cli, ["++foo", "--bar"])
assert not result.exception assert not result.exception
assert result.output == 'foo=True bar=True\n' assert result.output == "foo=True bar=True\n"
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert re.search(r'\+\+foo\s+das foo', result.output) is not None assert re.search(r"\+\+foo\s+das foo", result.output) is not None
assert re.search(r'--bar\s+das bar', result.output) is not None assert re.search(r"--bar\s+das bar", result.output) is not None
def test_invalid_option(runner): def test_invalid_option(runner):
try: with pytest.raises(TypeError, match="name was passed"):
@click.command() @click.command()
@click.option('foo') @click.option("foo")
def cli(foo): def cli(foo):
pass pass
except TypeError as e:
assert 'No options defined but a name was passed (foo).' \
in str(e)
else:
assert False, 'Expected a type error because of an invalid option.'
def test_invalid_nargs(runner): def test_invalid_nargs(runner):
try: with pytest.raises(TypeError, match="nargs < 0"):
@click.command() @click.command()
@click.option('--foo', nargs=-1) @click.option("--foo", nargs=-1)
def cli(foo): def cli(foo):
pass pass
except TypeError as e:
assert 'Options cannot have nargs < 0' in str(e)
else:
assert False, 'Expected a type error because of an invalid option.'
def test_nargs_tup_composite_mult(runner): def test_nargs_tup_composite_mult(runner):
@click.command() @click.command()
@click.option('--item', type=(str, int), multiple=True) @click.option("--item", type=(str, int), multiple=True)
def copy(item): def copy(item):
for item in item: for item in item:
click.echo('name=%s id=%d' % item) click.echo("name={0[0]} id={0[1]:d}".format(item))
result = runner.invoke(copy, ['--item', 'peter', '1', '--item', 'max', '2']) result = runner.invoke(copy, ["--item", "peter", "1", "--item", "max", "2"])
assert not result.exception assert not result.exception
assert result.output.splitlines() == [ assert result.output.splitlines() == ["name=peter id=1", "name=max id=2"]
'name=peter id=1',
'name=max id=2',
]
def test_counting(runner): def test_counting(runner):
@click.command() @click.command()
@click.option('-v', count=True, help='Verbosity', @click.option("-v", count=True, help="Verbosity", type=click.IntRange(0, 3))
type=click.IntRange(0, 3))
def cli(v): def cli(v):
click.echo('verbosity=%d' % v) click.echo("verbosity={:d}".format(v))
result = runner.invoke(cli, ['-vvv']) result = runner.invoke(cli, ["-vvv"])
assert not result.exception assert not result.exception
assert result.output == 'verbosity=3\n' assert result.output == "verbosity=3\n"
result = runner.invoke(cli, ['-vvvv']) result = runner.invoke(cli, ["-vvvv"])
assert result.exception assert result.exception
assert 'Invalid value for "-v": 4 is not in the valid range of 0 to 3.' \ assert (
"Invalid value for '-v': 4 is not in the valid range of 0 to 3."
in result.output in result.output
)
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert not result.exception assert not result.exception
assert result.output == 'verbosity=0\n' assert result.output == "verbosity=0\n"
result = runner.invoke(cli, ['--help']) result = runner.invoke(cli, ["--help"])
assert re.search(r'-v\s+Verbosity', result.output) is not None assert re.search(r"-v\s+Verbosity", result.output) is not None
@pytest.mark.parametrize('unknown_flag', ['--foo', '-f']) @pytest.mark.parametrize("unknown_flag", ["--foo", "-f"])
def test_unknown_options(runner, unknown_flag): def test_unknown_options(runner, unknown_flag):
@click.command() @click.command()
def cli(): def cli():
@ -95,74 +87,88 @@ def test_unknown_options(runner, unknown_flag):
result = runner.invoke(cli, [unknown_flag]) result = runner.invoke(cli, [unknown_flag])
assert result.exception assert result.exception
assert 'no such option: {0}'.format(unknown_flag) in result.output assert "no such option: {}".format(unknown_flag) in result.output
def test_multiple_required(runner): def test_multiple_required(runner):
@click.command() @click.command()
@click.option('-m', '--message', multiple=True, required=True) @click.option("-m", "--message", multiple=True, required=True)
def cli(message): def cli(message):
click.echo('\n'.join(message)) click.echo("\n".join(message))
result = runner.invoke(cli, ['-m', 'foo', '-mbar']) result = runner.invoke(cli, ["-m", "foo", "-mbar"])
assert not result.exception assert not result.exception
assert result.output == 'foo\nbar\n' assert result.output == "foo\nbar\n"
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.exception assert result.exception
assert 'Error: Missing option "-m" / "--message".' in result.output assert "Error: Missing option '-m' / '--message'." in result.output
def test_empty_envvar(runner):
@click.command()
@click.option("--mypath", type=click.Path(exists=True), envvar="MYPATH")
def cli(mypath):
click.echo("mypath: {}".format(mypath))
result = runner.invoke(cli, [], env={"MYPATH": ""})
assert result.exit_code == 0
assert result.output == "mypath: None\n"
def test_multiple_envvar(runner): def test_multiple_envvar(runner):
@click.command() @click.command()
@click.option('--arg', multiple=True) @click.option("--arg", multiple=True)
def cmd(arg): def cmd(arg):
click.echo('|'.join(arg)) click.echo("|".join(arg))
result = runner.invoke(cmd, [], auto_envvar_prefix='TEST', result = runner.invoke(
env={'TEST_ARG': 'foo bar baz'}) cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar baz"}
)
assert not result.exception assert not result.exception
assert result.output == 'foo|bar|baz\n' assert result.output == "foo|bar|baz\n"
@click.command() @click.command()
@click.option('--arg', multiple=True, envvar='X') @click.option("--arg", multiple=True, envvar="X")
def cmd(arg): def cmd(arg):
click.echo('|'.join(arg)) click.echo("|".join(arg))
result = runner.invoke(cmd, [], env={'X': 'foo bar baz'}) result = runner.invoke(cmd, [], env={"X": "foo bar baz"})
assert not result.exception assert not result.exception
assert result.output == 'foo|bar|baz\n' assert result.output == "foo|bar|baz\n"
@click.command() @click.command()
@click.option('--arg', multiple=True, type=click.Path()) @click.option("--arg", multiple=True, type=click.Path())
def cmd(arg): def cmd(arg):
click.echo('|'.join(arg)) click.echo("|".join(arg))
result = runner.invoke(cmd, [], auto_envvar_prefix='TEST', result = runner.invoke(
env={'TEST_ARG': 'foo%sbar' % os.path.pathsep}) cmd,
[],
auto_envvar_prefix="TEST",
env={"TEST_ARG": "foo{}bar".format(os.path.pathsep)},
)
assert not result.exception assert not result.exception
assert result.output == 'foo|bar\n' assert result.output == "foo|bar\n"
def test_multiple_default_help(runner): def test_multiple_default_help(runner):
@click.command() @click.command()
@click.option('--arg1', multiple=True, default=('foo', 'bar'), @click.option("--arg1", multiple=True, default=("foo", "bar"), show_default=True)
show_default=True) @click.option("--arg2", multiple=True, default=(1, 2), type=int, show_default=True)
@click.option('--arg2', multiple=True, default=(1, 2), type=int,
show_default=True)
def cmd(arg, arg2): def cmd(arg, arg2):
pass pass
result = runner.invoke(cmd, ['--help']) result = runner.invoke(cmd, ["--help"])
assert not result.exception assert not result.exception
assert 'foo, bar' in result.output assert "foo, bar" in result.output
assert '1, 2' in result.output assert "1, 2" in result.output
def test_multiple_default_type(runner): def test_multiple_default_type(runner):
@click.command() @click.command()
@click.option('--arg1', multiple=True, default=('foo', 'bar')) @click.option("--arg1", multiple=True, default=("foo", "bar"))
@click.option('--arg2', multiple=True, default=(1, 'a')) @click.option("--arg2", multiple=True, default=(1, "a"))
def cmd(arg1, arg2): def cmd(arg1, arg2):
assert all(isinstance(e[0], text_type) for e in arg1) assert all(isinstance(e[0], text_type) for e in arg1)
assert all(isinstance(e[1], text_type) for e in arg1) assert all(isinstance(e[1], text_type) for e in arg1)
@ -170,242 +176,292 @@ def test_multiple_default_type(runner):
assert all(isinstance(e[0], int) for e in arg2) assert all(isinstance(e[0], int) for e in arg2)
assert all(isinstance(e[1], text_type) for e in arg2) assert all(isinstance(e[1], text_type) for e in arg2)
result = runner.invoke(cmd, '--arg1 a b --arg1 test 1 --arg2 2 ' result = runner.invoke(
'two --arg2 4 four'.split()) cmd, "--arg1 a b --arg1 test 1 --arg2 2 two --arg2 4 four".split()
)
assert not result.exception assert not result.exception
def test_dynamic_default_help_unset(runner): def test_dynamic_default_help_unset(runner):
@click.command() @click.command()
@click.option('--username', prompt=True, @click.option(
default=lambda: os.environ.get('USER', ''), "--username",
show_default=True) prompt=True,
default=lambda: os.environ.get("USER", ""),
show_default=True,
)
def cmd(username): def cmd(username):
print("Hello,", username) print("Hello,", username)
result = runner.invoke(cmd, ['--help']) result = runner.invoke(cmd, ["--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert '--username' in result.output assert "--username" in result.output
assert 'lambda' not in result.output assert "lambda" not in result.output
assert '(dynamic)' in result.output assert "(dynamic)" in result.output
def test_dynamic_default_help_text(runner): def test_dynamic_default_help_text(runner):
@click.command() @click.command()
@click.option('--username', prompt=True, @click.option(
default=lambda: os.environ.get('USER', ''), "--username",
show_default='current user') prompt=True,
default=lambda: os.environ.get("USER", ""),
show_default="current user",
)
def cmd(username): def cmd(username):
print("Hello,", username) print("Hello,", username)
result = runner.invoke(cmd, ['--help']) result = runner.invoke(cmd, ["--help"])
assert result.exit_code == 0 assert result.exit_code == 0
assert '--username' in result.output assert "--username" in result.output
assert 'lambda' not in result.output assert "lambda" not in result.output
assert '(current user)' in result.output assert "(current user)" in result.output
def test_toupper_envvar_prefix(runner): def test_toupper_envvar_prefix(runner):
@click.command() @click.command()
@click.option('--arg') @click.option("--arg")
def cmd(arg): def cmd(arg):
click.echo(arg) click.echo(arg)
result = runner.invoke(cmd, [], auto_envvar_prefix='test', result = runner.invoke(cmd, [], auto_envvar_prefix="test", env={"TEST_ARG": "foo"})
env={'TEST_ARG': 'foo'})
assert not result.exception assert not result.exception
assert result.output == 'foo\n' assert result.output == "foo\n"
def test_nargs_envvar(runner): def test_nargs_envvar(runner):
@click.command() @click.command()
@click.option('--arg', nargs=2) @click.option("--arg", nargs=2)
def cmd(arg): def cmd(arg):
click.echo('|'.join(arg)) click.echo("|".join(arg))
result = runner.invoke(cmd, [], auto_envvar_prefix='TEST', result = runner.invoke(
env={'TEST_ARG': 'foo bar'}) cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "foo bar"}
)
assert not result.exception assert not result.exception
assert result.output == 'foo|bar\n' assert result.output == "foo|bar\n"
@click.command() @click.command()
@click.option('--arg', nargs=2, multiple=True) @click.option("--arg", nargs=2, multiple=True)
def cmd(arg): def cmd(arg):
for item in arg: for item in arg:
click.echo('|'.join(item)) click.echo("|".join(item))
result = runner.invoke(cmd, [], auto_envvar_prefix='TEST', result = runner.invoke(
env={'TEST_ARG': 'x 1 y 2'}) cmd, [], auto_envvar_prefix="TEST", env={"TEST_ARG": "x 1 y 2"}
)
assert not result.exception assert not result.exception
assert result.output == 'x|1\ny|2\n' assert result.output == "x|1\ny|2\n"
def test_show_envvar(runner): def test_show_envvar(runner):
@click.command() @click.command()
@click.option('--arg1', envvar='ARG1', @click.option("--arg1", envvar="ARG1", show_envvar=True)
show_envvar=True)
def cmd(arg): def cmd(arg):
pass pass
result = runner.invoke(cmd, ['--help']) result = runner.invoke(cmd, ["--help"])
assert not result.exception assert not result.exception
assert 'ARG1' in result.output assert "ARG1" in result.output
def test_show_envvar_auto_prefix(runner): def test_show_envvar_auto_prefix(runner):
@click.command() @click.command()
@click.option('--arg1', show_envvar=True) @click.option("--arg1", show_envvar=True)
def cmd(arg): def cmd(arg):
pass pass
result = runner.invoke(cmd, ['--help'], result = runner.invoke(cmd, ["--help"], auto_envvar_prefix="TEST")
auto_envvar_prefix='TEST')
assert not result.exception assert not result.exception
assert 'TEST_ARG1' in result.output assert "TEST_ARG1" in result.output
def test_show_envvar_auto_prefix_dash_in_command(runner):
@click.group()
def cli():
pass
@cli.command()
@click.option("--baz", show_envvar=True)
def foo_bar(baz):
pass
result = runner.invoke(cli, ["foo-bar", "--help"], auto_envvar_prefix="TEST")
assert not result.exception
assert "TEST_FOO_BAR_BAZ" in result.output
def test_custom_validation(runner): def test_custom_validation(runner):
def validate_pos_int(ctx, param, value): def validate_pos_int(ctx, param, value):
if value < 0: if value < 0:
raise click.BadParameter('Value needs to be positive') raise click.BadParameter("Value needs to be positive")
return value return value
@click.command() @click.command()
@click.option('--foo', callback=validate_pos_int, default=1) @click.option("--foo", callback=validate_pos_int, default=1)
def cmd(foo): def cmd(foo):
click.echo(foo) click.echo(foo)
result = runner.invoke(cmd, ['--foo', '-1']) result = runner.invoke(cmd, ["--foo", "-1"])
assert 'Invalid value for "--foo": Value needs to be positive' \ assert "Invalid value for '--foo': Value needs to be positive" in result.output
in result.output
result = runner.invoke(cmd, ['--foo', '42']) result = runner.invoke(cmd, ["--foo", "42"])
assert result.output == '42\n' assert result.output == "42\n"
def test_winstyle_options(runner): def test_winstyle_options(runner):
@click.command() @click.command()
@click.option('/debug;/no-debug', help='Enables or disables debug mode.') @click.option("/debug;/no-debug", help="Enables or disables debug mode.")
def cmd(debug): def cmd(debug):
click.echo(debug) click.echo(debug)
result = runner.invoke(cmd, ['/debug'], help_option_names=['/?']) result = runner.invoke(cmd, ["/debug"], help_option_names=["/?"])
assert result.output == 'True\n' assert result.output == "True\n"
result = runner.invoke(cmd, ['/no-debug'], help_option_names=['/?']) result = runner.invoke(cmd, ["/no-debug"], help_option_names=["/?"])
assert result.output == 'False\n' assert result.output == "False\n"
result = runner.invoke(cmd, [], help_option_names=['/?']) result = runner.invoke(cmd, [], help_option_names=["/?"])
assert result.output == 'False\n' assert result.output == "False\n"
result = runner.invoke(cmd, ['/?'], help_option_names=['/?']) result = runner.invoke(cmd, ["/?"], help_option_names=["/?"])
assert '/debug; /no-debug Enables or disables debug mode.' in result.output assert "/debug; /no-debug Enables or disables debug mode." in result.output
assert '/? Show this message and exit.' in result.output assert "/? Show this message and exit." in result.output
def test_legacy_options(runner): def test_legacy_options(runner):
@click.command() @click.command()
@click.option('-whatever') @click.option("-whatever")
def cmd(whatever): def cmd(whatever):
click.echo(whatever) click.echo(whatever)
result = runner.invoke(cmd, ['-whatever', '42']) result = runner.invoke(cmd, ["-whatever", "42"])
assert result.output == '42\n' assert result.output == "42\n"
result = runner.invoke(cmd, ['-whatever=23']) result = runner.invoke(cmd, ["-whatever=23"])
assert result.output == '23\n' assert result.output == "23\n"
def test_missing_option_string_cast():
ctx = click.Context(click.Command(""))
with pytest.raises(click.MissingParameter) as excinfo:
click.Option(["-a"], required=True).full_process_value(ctx, None)
assert str(excinfo.value) == "missing parameter: a"
def test_missing_choice(runner): def test_missing_choice(runner):
@click.command() @click.command()
@click.option('--foo', type=click.Choice(['foo', 'bar']), @click.option("--foo", type=click.Choice(["foo", "bar"]), required=True)
required=True)
def cmd(foo): def cmd(foo):
click.echo(foo) click.echo(foo)
result = runner.invoke(cmd) result = runner.invoke(cmd)
assert result.exit_code == 2 assert result.exit_code == 2
error, separator, choices = result.output.partition('Choose from') error, separator, choices = result.output.partition("Choose from")
assert 'Error: Missing option "--foo". ' in error assert "Error: Missing option '--foo'. " in error
assert 'Choose from' in separator assert "Choose from" in separator
assert 'foo' in choices assert "foo" in choices
assert 'bar' in choices assert "bar" in choices
def test_case_insensitive_choice(runner): def test_case_insensitive_choice(runner):
@click.command() @click.command()
@click.option('--foo', type=click.Choice( @click.option("--foo", type=click.Choice(["Orange", "Apple"], case_sensitive=False))
['Orange', 'Apple'], case_sensitive=False))
def cmd(foo): def cmd(foo):
click.echo(foo) click.echo(foo)
result = runner.invoke(cmd, ['--foo', 'apple']) result = runner.invoke(cmd, ["--foo", "apple"])
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == "Apple\n"
result = runner.invoke(cmd, ['--foo', 'oRANGe']) result = runner.invoke(cmd, ["--foo", "oRANGe"])
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == "Orange\n"
result = runner.invoke(cmd, ['--foo', 'Apple']) result = runner.invoke(cmd, ["--foo", "Apple"])
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == "Apple\n"
@click.command() @click.command()
@click.option('--foo', type=click.Choice(['Orange', 'Apple'])) @click.option("--foo", type=click.Choice(["Orange", "Apple"]))
def cmd2(foo): def cmd2(foo):
click.echo(foo) click.echo(foo)
result = runner.invoke(cmd2, ['--foo', 'apple']) result = runner.invoke(cmd2, ["--foo", "apple"])
assert result.exit_code == 2 assert result.exit_code == 2
result = runner.invoke(cmd2, ['--foo', 'oRANGe']) result = runner.invoke(cmd2, ["--foo", "oRANGe"])
assert result.exit_code == 2 assert result.exit_code == 2
result = runner.invoke(cmd2, ['--foo', 'Apple']) result = runner.invoke(cmd2, ["--foo", "Apple"])
assert result.exit_code == 0 assert result.exit_code == 0
def test_multiline_help(runner): def test_case_insensitive_choice_returned_exactly(runner):
@click.command() @click.command()
@click.option('--foo', help=""" @click.option("--foo", type=click.Choice(["Orange", "Apple"], case_sensitive=False))
hello
i am
multiline
""")
def cmd(foo): def cmd(foo):
click.echo(foo) click.echo(foo)
result = runner.invoke(cmd, ['--help']) result = runner.invoke(cmd, ["--foo", "apple"])
assert result.exit_code == 0 assert result.exit_code == 0
out = result.output.splitlines() assert result.output == "Apple\n"
assert ' --foo TEXT hello' in out
assert ' i am' in out
assert ' multiline' in out def test_option_help_preserve_paragraphs(runner):
@click.command()
@click.option(
"-C",
"--config",
type=click.Path(),
help="""Configuration file to use.
If not given, the environment variable CONFIG_FILE is consulted
and used if set. If neither are given, a default configuration
file is loaded.""",
)
def cmd(config):
pass
result = runner.invoke(cmd, ["--help"],)
assert result.exit_code == 0
assert (
" -C, --config PATH Configuration file to use.\n"
"{i}\n"
"{i}If not given, the environment variable CONFIG_FILE is\n"
"{i}consulted and used if set. If neither are given, a default\n"
"{i}configuration file is loaded.".format(i=" " * 21)
) in result.output
def test_argument_custom_class(runner): def test_argument_custom_class(runner):
class CustomArgument(click.Argument): class CustomArgument(click.Argument):
def get_default(self, ctx): def get_default(self, ctx):
'''a dumb override of a default value for testing''' """a dumb override of a default value for testing"""
return 'I am a default' return "I am a default"
@click.command() @click.command()
@click.argument('testarg', cls=CustomArgument, default='you wont see me') @click.argument("testarg", cls=CustomArgument, default="you wont see me")
def cmd(testarg): def cmd(testarg):
click.echo(testarg) click.echo(testarg)
result = runner.invoke(cmd) result = runner.invoke(cmd)
assert 'I am a default' in result.output assert "I am a default" in result.output
assert 'you wont see me' not in result.output assert "you wont see me" not in result.output
def test_option_custom_class(runner): def test_option_custom_class(runner):
class CustomOption(click.Option): class CustomOption(click.Option):
def get_help_record(self, ctx): def get_help_record(self, ctx):
'''a dumb override of a help text for testing''' """a dumb override of a help text for testing"""
return ('--help', 'I am a help text') return ("--help", "I am a help text")
@click.command() @click.command()
@click.option('--testoption', cls=CustomOption, help='you wont see me') @click.option("--testoption", cls=CustomOption, help="you wont see me")
def cmd(testoption): def cmd(testoption):
click.echo(testoption) click.echo(testoption)
result = runner.invoke(cmd, ['--help']) result = runner.invoke(cmd, ["--help"])
assert 'I am a help text' in result.output assert "I am a help text" in result.output
assert 'you wont see me' not in result.output assert "you wont see me" not in result.output
def test_option_custom_class_reusable(runner): def test_option_custom_class_reusable(runner):
@ -413,11 +469,11 @@ def test_option_custom_class_reusable(runner):
class CustomOption(click.Option): class CustomOption(click.Option):
def get_help_record(self, ctx): def get_help_record(self, ctx):
'''a dumb override of a help text for testing''' """a dumb override of a help text for testing"""
return ('--help', 'I am a help text') return ("--help", "I am a help text")
# Assign to a variable to re-use the decorator. # Assign to a variable to re-use the decorator.
testoption = click.option('--testoption', cls=CustomOption, help='you wont see me') testoption = click.option("--testoption", cls=CustomOption, help="you wont see me")
@click.command() @click.command()
@testoption @testoption
@ -432,50 +488,63 @@ def test_option_custom_class_reusable(runner):
# Both of the commands should have the --help option now. # Both of the commands should have the --help option now.
for cmd in (cmd1, cmd2): for cmd in (cmd1, cmd2):
result = runner.invoke(cmd, ['--help']) result = runner.invoke(cmd, ["--help"])
assert 'I am a help text' in result.output assert "I am a help text" in result.output
assert 'you wont see me' not in result.output assert "you wont see me" not in result.output
def test_bool_flag_with_type(runner):
@click.command()
@click.option("--shout/--no-shout", default=False, type=bool)
def cmd(shout):
pass
result = runner.invoke(cmd)
assert not result.exception
def test_aliases_for_flags(runner): def test_aliases_for_flags(runner):
@click.command() @click.command()
@click.option('--warnings/--no-warnings', ' /-W', default=True) @click.option("--warnings/--no-warnings", " /-W", default=True)
def cli(warnings): def cli(warnings):
click.echo(warnings) click.echo(warnings)
result = runner.invoke(cli, ['--warnings']) result = runner.invoke(cli, ["--warnings"])
assert result.output == 'True\n' assert result.output == "True\n"
result = runner.invoke(cli, ['--no-warnings']) result = runner.invoke(cli, ["--no-warnings"])
assert result.output == 'False\n' assert result.output == "False\n"
result = runner.invoke(cli, ['-W']) result = runner.invoke(cli, ["-W"])
assert result.output == 'False\n' assert result.output == "False\n"
@click.command() @click.command()
@click.option('--warnings/--no-warnings', '-w', default=True) @click.option("--warnings/--no-warnings", "-w", default=True)
def cli_alt(warnings): def cli_alt(warnings):
click.echo(warnings) click.echo(warnings)
result = runner.invoke(cli_alt, ['--warnings']) result = runner.invoke(cli_alt, ["--warnings"])
assert result.output == 'True\n' assert result.output == "True\n"
result = runner.invoke(cli_alt, ['--no-warnings']) result = runner.invoke(cli_alt, ["--no-warnings"])
assert result.output == 'False\n' assert result.output == "False\n"
result = runner.invoke(cli_alt, ['-w']) result = runner.invoke(cli_alt, ["-w"])
assert result.output == 'True\n' assert result.output == "True\n"
@pytest.mark.parametrize('option_args,expected', [
(['--aggressive', '--all', '-a'], 'aggressive'), @pytest.mark.parametrize(
(['--first', '--second', '--third', '-a', '-b', '-c'], 'first'), "option_args,expected",
(['--apple', '--banana', '--cantaloupe', '-a', '-b', '-c'], 'apple'), [
(['--cantaloupe', '--banana', '--apple', '-c', '-b', '-a'], 'cantaloupe'), (["--aggressive", "--all", "-a"], "aggressive"),
(['-a', '-b', '-c'], 'a'), (["--first", "--second", "--third", "-a", "-b", "-c"], "first"),
(['-c', '-b', '-a'], 'c'), (["--apple", "--banana", "--cantaloupe", "-a", "-b", "-c"], "apple"),
(['-a', '--apple', '-b', '--banana', '-c', '--cantaloupe'], 'apple'), (["--cantaloupe", "--banana", "--apple", "-c", "-b", "-a"], "cantaloupe"),
(['-c', '-a', '--cantaloupe', '-b', '--banana', '--apple'], 'cantaloupe'), (["-a", "-b", "-c"], "a"),
(['--from', '-f', '_from'], '_from'), (["-c", "-b", "-a"], "c"),
(['--return', '-r', '_ret'], '_ret'), (["-a", "--apple", "-b", "--banana", "-c", "--cantaloupe"], "apple"),
]) (["-c", "-a", "--cantaloupe", "-b", "--banana", "--apple"], "cantaloupe"),
(["--from", "-f", "_from"], "_from"),
(["--return", "-r", "_ret"], "_ret"),
],
)
def test_option_names(runner, option_args, expected): def test_option_names(runner, option_args, expected):
@click.command() @click.command()
@click.option(*option_args, is_flag=True) @click.option(*option_args, is_flag=True)
def cmd(**kwargs): def cmd(**kwargs):
@ -484,6 +553,6 @@ def test_option_names(runner, option_args, expected):
assert cmd.params[0].name == expected assert cmd.params[0].name == expected
for form in option_args: for form in option_args:
if form.startswith('-'): if form.startswith("-"):
result = runner.invoke(cmd, [form]) result = runner.invoke(cmd, [form])
assert result.output == 'True\n' assert result.output == "True\n"

View file

@ -1,6 +1,11 @@
import click # -*- coding: utf-8 -*-
import time import time
import pytest
import click._termui_impl
from click._compat import WIN
class FakeClock(object): class FakeClock(object):
def __init__(self): def __init__(self):
@ -13,18 +18,26 @@ class FakeClock(object):
return self.now return self.now
def _create_progress(length=10, length_known=True, **kwargs):
progress = click.progressbar(tuple(range(length)))
for key, value in kwargs.items():
setattr(progress, key, value)
progress.length_known = length_known
return progress
def test_progressbar_strip_regression(runner, monkeypatch): def test_progressbar_strip_regression(runner, monkeypatch):
fake_clock = FakeClock() fake_clock = FakeClock()
label = ' padded line' label = " padded line"
@click.command() @click.command()
def cli(): def cli():
with click.progressbar(tuple(range(10)), label=label) as progress: with _create_progress(label=label) as progress:
for thing in progress: for _ in progress:
fake_clock.advance_time() fake_clock.advance_time()
monkeypatch.setattr(time, 'time', fake_clock.time) monkeypatch.setattr(time, "time", fake_clock.time)
monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: True) monkeypatch.setattr(click._termui_impl, "isatty", lambda _: True)
assert label in runner.invoke(cli, []).output assert label in runner.invoke(cli, []).output
@ -51,56 +64,198 @@ def test_progressbar_length_hint(runner, monkeypatch):
@click.command() @click.command()
def cli(): def cli():
with click.progressbar(Hinted(10), label='test') as progress: with click.progressbar(Hinted(10), label="test") as progress:
for thing in progress: for _ in progress:
fake_clock.advance_time() fake_clock.advance_time()
monkeypatch.setattr(time, 'time', fake_clock.time) monkeypatch.setattr(time, "time", fake_clock.time)
monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: True) monkeypatch.setattr(click._termui_impl, "isatty", lambda _: True)
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.exception is None assert result.exception is None
def test_progressbar_hidden(runner, monkeypatch): def test_progressbar_hidden(runner, monkeypatch):
fake_clock = FakeClock() fake_clock = FakeClock()
label = 'whatever' label = "whatever"
@click.command() @click.command()
def cli(): def cli():
with click.progressbar(tuple(range(10)), label=label) as progress: with _create_progress(label=label) as progress:
for thing in progress: for _ in progress:
fake_clock.advance_time() fake_clock.advance_time()
monkeypatch.setattr(time, 'time', fake_clock.time) monkeypatch.setattr(time, "time", fake_clock.time)
monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: False) monkeypatch.setattr(click._termui_impl, "isatty", lambda _: False)
assert runner.invoke(cli, []).output == '' assert runner.invoke(cli, []).output == ""
@pytest.mark.parametrize("avg, expected", [([], 0.0), ([1, 4], 2.5)])
def test_progressbar_time_per_iteration(runner, avg, expected):
with _create_progress(2, avg=avg) as progress:
assert progress.time_per_iteration == expected
@pytest.mark.parametrize("finished, expected", [(False, 5), (True, 0)])
def test_progressbar_eta(runner, finished, expected):
with _create_progress(2, finished=finished, avg=[1, 4]) as progress:
assert progress.eta == expected
@pytest.mark.parametrize(
"eta, expected",
[
(0, "00:00:00"),
(30, "00:00:30"),
(90, "00:01:30"),
(900, "00:15:00"),
(9000, "02:30:00"),
(99999999999, "1157407d 09:46:39"),
(None, ""),
],
)
def test_progressbar_format_eta(runner, eta, expected):
with _create_progress(1, eta_known=eta is not None, avg=[eta]) as progress:
assert progress.format_eta() == expected
@pytest.mark.parametrize("pos, length", [(0, 5), (-1, 1), (5, 5), (6, 5), (4, 0)])
def test_progressbar_format_pos(runner, pos, length):
with _create_progress(length, length_known=length != 0, pos=pos) as progress:
result = progress.format_pos()
if progress.length_known:
assert result == "{}/{}".format(pos, length)
else:
assert result == str(pos)
@pytest.mark.parametrize(
"length, finished, pos, avg, expected",
[
(8, False, 7, 0, "#######-"),
(0, True, 8, 0, "########"),
(0, False, 8, 0, "--------"),
(0, False, 5, 3, "#-------"),
],
)
def test_progressbar_format_bar(runner, length, finished, pos, avg, expected):
with _create_progress(
length, length_known=length != 0, width=8, pos=pos, finished=finished, avg=[avg]
) as progress:
assert progress.format_bar() == expected
@pytest.mark.parametrize(
"length, length_known, show_percent, show_pos, pos, expected",
[
(0, True, True, True, 0, " [--------] 0/0 0%"),
(0, True, False, True, 0, " [--------] 0/0"),
(0, True, False, False, 0, " [--------]"),
(0, False, False, False, 0, " [--------]"),
(8, True, True, True, 8, " [########] 8/8 100%"),
],
)
def test_progressbar_format_progress_line(
runner, length, length_known, show_percent, show_pos, pos, expected
):
with _create_progress(
length,
length_known,
width=8,
show_percent=show_percent,
pos=pos,
show_pos=show_pos,
) as progress:
assert progress.format_progress_line() == expected
@pytest.mark.parametrize("test_item", ["test", None])
def test_progressbar_format_progress_line_with_show_func(runner, test_item):
def item_show_func(item):
return item
with _create_progress(
item_show_func=item_show_func, current_item=test_item
) as progress:
if test_item:
assert progress.format_progress_line().endswith(test_item)
else:
assert progress.format_progress_line().endswith(progress.format_pct())
def test_progressbar_init_exceptions(runner):
with pytest.raises(TypeError, match="iterable or length is required"):
click.progressbar()
def test_progressbar_iter_outside_with_exceptions(runner):
with pytest.raises(RuntimeError, match="with block"):
progress = click.progressbar(length=2)
iter(progress)
def test_progressbar_is_iterator(runner, monkeypatch):
fake_clock = FakeClock()
@click.command()
def cli():
with click.progressbar(range(10), label="test") as progress:
while True:
try:
next(progress)
fake_clock.advance_time()
except StopIteration:
break
monkeypatch.setattr(time, "time", fake_clock.time)
monkeypatch.setattr(click._termui_impl, "isatty", lambda _: True)
result = runner.invoke(cli, [])
assert result.exception is None
def test_choices_list_in_prompt(runner, monkeypatch): def test_choices_list_in_prompt(runner, monkeypatch):
@click.command() @click.command()
@click.option('-g', type=click.Choice(['none', 'day', 'week', 'month']), @click.option(
prompt=True) "-g", type=click.Choice(["none", "day", "week", "month"]), prompt=True
)
def cli_with_choices(g): def cli_with_choices(g):
pass pass
@click.command() @click.command()
@click.option('-g', type=click.Choice(['none', 'day', 'week', 'month']), @click.option(
prompt=True, show_choices=False) "-g",
type=click.Choice(["none", "day", "week", "month"]),
prompt=True,
show_choices=False,
)
def cli_without_choices(g): def cli_without_choices(g):
pass pass
result = runner.invoke(cli_with_choices, [], input='none') result = runner.invoke(cli_with_choices, [], input="none")
assert '(none, day, week, month)' in result.output assert "(none, day, week, month)" in result.output
result = runner.invoke(cli_without_choices, [], input='none') result = runner.invoke(cli_without_choices, [], input="none")
assert '(none, day, week, month)' not in result.output assert "(none, day, week, month)" not in result.output
@pytest.mark.parametrize(
"file_kwargs", [{"mode": "rt"}, {"mode": "rb"}, {"lazy": True}]
)
def test_file_prompt_default_format(runner, file_kwargs):
@click.command()
@click.option("-f", default=__file__, prompt="file", type=click.File(**file_kwargs))
def cli(f):
click.echo(f.name)
result = runner.invoke(cli)
assert result.output == "file [{0}]: \n{0}\n".format(__file__)
def test_secho(runner): def test_secho(runner):
with runner.isolation() as outstreams: with runner.isolation() as outstreams:
click.secho(None, nl=False) click.secho(None, nl=False)
bytes = outstreams[0].getvalue() bytes = outstreams[0].getvalue()
assert bytes == b'' assert bytes == b""
def test_progressbar_yields_all_items(runner): def test_progressbar_yields_all_items(runner):
@ -118,13 +273,50 @@ def test_progressbar_update(runner, monkeypatch):
fake_clock.advance_time() fake_clock.advance_time()
print("") print("")
monkeypatch.setattr(time, 'time', fake_clock.time) monkeypatch.setattr(time, "time", fake_clock.time)
monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: True) monkeypatch.setattr(click._termui_impl, "isatty", lambda _: True)
output = runner.invoke(cli, []).output output = runner.invoke(cli, []).output
lines = [line for line in output.split('\n') if '[' in line] lines = [line for line in output.split("\n") if "[" in line]
assert ' 25% 00:00:03' in lines[0] assert " 25% 00:00:03" in lines[0]
assert ' 50% 00:00:02' in lines[1] assert " 50% 00:00:02" in lines[1]
assert ' 75% 00:00:01' in lines[2] assert " 75% 00:00:01" in lines[2]
assert '100% ' in lines[3] assert "100% " in lines[3]
@pytest.mark.parametrize("key_char", (u"h", u"H", u"é", u"À", u" ", u"", u"àH", u"àR"))
@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(click.termui, "_getchar", None)
assert click.getchar(echo) == key_char
@pytest.mark.parametrize(
"special_key_char, key_char", [(u"\x00", "a"), (u"\x00", "b"), (u"\xe0", "c")]
)
@pytest.mark.skipif(
not WIN, reason="Tests special character inputs using the msvcrt module."
)
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()
)
monkeypatch.setattr(click.termui, "_getchar", None)
assert click.getchar() == special_key_char + key_char
@pytest.mark.parametrize(
("key_char", "exc"), [(u"\x03", KeyboardInterrupt), (u"\x1a", EOFError)],
)
@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(click.termui, "_getchar", None)
with pytest.raises(exc):
click.getchar()

View file

@ -2,12 +2,12 @@ import os
import sys import sys
import pytest import pytest
import click import click
from click._compat import PY2
from click._compat import WIN
from click.testing import CliRunner from click.testing import CliRunner
from click._compat import PY2, WIN
# Use the most reasonable io that users would use for the python version. # Use the most reasonable io that users would use for the python version.
if PY2: if PY2:
from cStringIO import StringIO as ReasonableBytesIO from cStringIO import StringIO as ReasonableBytesIO
@ -18,8 +18,8 @@ else:
def test_runner(): def test_runner():
@click.command() @click.command()
def test(): def test():
i = click.get_binary_stream('stdin') i = click.get_binary_stream("stdin")
o = click.get_binary_stream('stdout') o = click.get_binary_stream("stdout")
while 1: while 1:
chunk = i.read(4096) chunk = i.read(4096)
if not chunk: if not chunk:
@ -28,21 +28,21 @@ def test_runner():
o.flush() o.flush()
runner = CliRunner() runner = CliRunner()
result = runner.invoke(test, input='Hello World!\n') result = runner.invoke(test, input="Hello World!\n")
assert not result.exception assert not result.exception
assert result.output == 'Hello World!\n' assert result.output == "Hello World!\n"
runner = CliRunner(echo_stdin=True) runner = CliRunner(echo_stdin=True)
result = runner.invoke(test, input='Hello World!\n') result = runner.invoke(test, input="Hello World!\n")
assert not result.exception assert not result.exception
assert result.output == 'Hello World!\nHello World!\n' assert result.output == "Hello World!\nHello World!\n"
def test_runner_with_stream(): def test_runner_with_stream():
@click.command() @click.command()
def test(): def test():
i = click.get_binary_stream('stdin') i = click.get_binary_stream("stdin")
o = click.get_binary_stream('stdout') o = click.get_binary_stream("stdout")
while 1: while 1:
chunk = i.read(4096) chunk = i.read(4096)
if not chunk: if not chunk:
@ -51,36 +51,36 @@ def test_runner_with_stream():
o.flush() o.flush()
runner = CliRunner() runner = CliRunner()
result = runner.invoke(test, input=ReasonableBytesIO(b'Hello World!\n')) result = runner.invoke(test, input=ReasonableBytesIO(b"Hello World!\n"))
assert not result.exception assert not result.exception
assert result.output == 'Hello World!\n' assert result.output == "Hello World!\n"
runner = CliRunner(echo_stdin=True) runner = CliRunner(echo_stdin=True)
result = runner.invoke(test, input=ReasonableBytesIO(b'Hello World!\n')) result = runner.invoke(test, input=ReasonableBytesIO(b"Hello World!\n"))
assert not result.exception assert not result.exception
assert result.output == 'Hello World!\nHello World!\n' assert result.output == "Hello World!\nHello World!\n"
def test_prompts(): def test_prompts():
@click.command() @click.command()
@click.option('--foo', prompt=True) @click.option("--foo", prompt=True)
def test(foo): def test(foo):
click.echo('foo=%s' % foo) click.echo("foo={}".format(foo))
runner = CliRunner() runner = CliRunner()
result = runner.invoke(test, input='wau wau\n') result = runner.invoke(test, input="wau wau\n")
assert not result.exception assert not result.exception
assert result.output == 'Foo: wau wau\nfoo=wau wau\n' assert result.output == "Foo: wau wau\nfoo=wau wau\n"
@click.command() @click.command()
@click.option('--foo', prompt=True, hide_input=True) @click.option("--foo", prompt=True, hide_input=True)
def test(foo): def test(foo):
click.echo('foo=%s' % foo) click.echo("foo={}".format(foo))
runner = CliRunner() runner = CliRunner()
result = runner.invoke(test, input='wau wau\n') result = runner.invoke(test, input="wau wau\n")
assert not result.exception assert not result.exception
assert result.output == 'Foo: \nfoo=wau wau\n' assert result.output == "Foo: \nfoo=wau wau\n"
def test_getchar(): def test_getchar():
@ -89,9 +89,9 @@ def test_getchar():
click.echo(click.getchar()) click.echo(click.getchar())
runner = CliRunner() runner = CliRunner()
result = runner.invoke(continue_it, input='y') result = runner.invoke(continue_it, input="y")
assert not result.exception assert not result.exception
assert result.output == 'y\n' assert result.output == "y\n"
def test_catch_exceptions(): def test_catch_exceptions():
@ -118,20 +118,20 @@ def test_catch_exceptions():
assert result.exit_code == 1 assert result.exit_code == 1
@pytest.mark.skipif(WIN, reason='Test does not make sense on Windows.') @pytest.mark.skipif(WIN, reason="Test does not make sense on Windows.")
def test_with_color(): def test_with_color():
@click.command() @click.command()
def cli(): def cli():
click.secho('hello world', fg='blue') click.secho("hello world", fg="blue")
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli) result = runner.invoke(cli)
assert result.output == 'hello world\n' assert result.output == "hello world\n"
assert not result.exception assert not result.exception
result = runner.invoke(cli, color=True) result = runner.invoke(cli, color=True)
assert result.output == click.style('hello world', fg='blue') + '\n' assert result.output == "{}\n".format(click.style("hello world", fg="blue"))
assert not result.exception assert not result.exception
@ -143,93 +143,93 @@ def test_with_color_but_pause_not_blocking():
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, color=True) result = runner.invoke(cli, color=True)
assert not result.exception assert not result.exception
assert result.output == '' assert result.output == ""
def test_exit_code_and_output_from_sys_exit(): def test_exit_code_and_output_from_sys_exit():
# See issue #362 # See issue #362
@click.command() @click.command()
def cli_string(): def cli_string():
click.echo('hello world') click.echo("hello world")
sys.exit('error') sys.exit("error")
@click.command() @click.command()
@click.pass_context @click.pass_context
def cli_string_ctx_exit(ctx): def cli_string_ctx_exit(ctx):
click.echo('hello world') click.echo("hello world")
ctx.exit('error') ctx.exit("error")
@click.command() @click.command()
def cli_int(): def cli_int():
click.echo('hello world') click.echo("hello world")
sys.exit(1) sys.exit(1)
@click.command() @click.command()
@click.pass_context @click.pass_context
def cli_int_ctx_exit(ctx): def cli_int_ctx_exit(ctx):
click.echo('hello world') click.echo("hello world")
ctx.exit(1) ctx.exit(1)
@click.command() @click.command()
def cli_float(): def cli_float():
click.echo('hello world') click.echo("hello world")
sys.exit(1.0) sys.exit(1.0)
@click.command() @click.command()
@click.pass_context @click.pass_context
def cli_float_ctx_exit(ctx): def cli_float_ctx_exit(ctx):
click.echo('hello world') click.echo("hello world")
ctx.exit(1.0) ctx.exit(1.0)
@click.command() @click.command()
def cli_no_error(): def cli_no_error():
click.echo('hello world') click.echo("hello world")
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli_string) result = runner.invoke(cli_string)
assert result.exit_code == 1 assert result.exit_code == 1
assert result.output == 'hello world\nerror\n' assert result.output == "hello world\nerror\n"
result = runner.invoke(cli_string_ctx_exit) result = runner.invoke(cli_string_ctx_exit)
assert result.exit_code == 1 assert result.exit_code == 1
assert result.output == 'hello world\nerror\n' assert result.output == "hello world\nerror\n"
result = runner.invoke(cli_int) result = runner.invoke(cli_int)
assert result.exit_code == 1 assert result.exit_code == 1
assert result.output == 'hello world\n' assert result.output == "hello world\n"
result = runner.invoke(cli_int_ctx_exit) result = runner.invoke(cli_int_ctx_exit)
assert result.exit_code == 1 assert result.exit_code == 1
assert result.output == 'hello world\n' assert result.output == "hello world\n"
result = runner.invoke(cli_float) result = runner.invoke(cli_float)
assert result.exit_code == 1 assert result.exit_code == 1
assert result.output == 'hello world\n1.0\n' assert result.output == "hello world\n1.0\n"
result = runner.invoke(cli_float_ctx_exit) result = runner.invoke(cli_float_ctx_exit)
assert result.exit_code == 1 assert result.exit_code == 1
assert result.output == 'hello world\n1.0\n' assert result.output == "hello world\n1.0\n"
result = runner.invoke(cli_no_error) result = runner.invoke(cli_no_error)
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'hello world\n' assert result.output == "hello world\n"
def test_env(): def test_env():
@click.command() @click.command()
def cli_env(): def cli_env():
click.echo('ENV=%s' % os.environ['TEST_CLICK_ENV']) click.echo("ENV={}".format(os.environ["TEST_CLICK_ENV"]))
runner = CliRunner() runner = CliRunner()
env_orig = dict(os.environ) env_orig = dict(os.environ)
env = dict(env_orig) env = dict(env_orig)
assert 'TEST_CLICK_ENV' not in env assert "TEST_CLICK_ENV" not in env
env['TEST_CLICK_ENV'] = 'some_value' env["TEST_CLICK_ENV"] = "some_value"
result = runner.invoke(cli_env, env=env) result = runner.invoke(cli_env, env=env)
assert result.exit_code == 0 assert result.exit_code == 0
assert result.output == 'ENV=some_value\n' assert result.output == "ENV=some_value\n"
assert os.environ == env_orig assert os.environ == env_orig
@ -244,31 +244,45 @@ def test_stderr():
result = runner.invoke(cli_stderr) result = runner.invoke(cli_stderr)
assert result.output == 'stdout\n' assert result.output == "stdout\n"
assert result.stdout == 'stdout\n' assert result.stdout == "stdout\n"
assert result.stderr == 'stderr\n' assert result.stderr == "stderr\n"
runner_mix = CliRunner(mix_stderr=True) runner_mix = CliRunner(mix_stderr=True)
result_mix = runner_mix.invoke(cli_stderr) result_mix = runner_mix.invoke(cli_stderr)
assert result_mix.output == 'stdout\nstderr\n' assert result_mix.output == "stdout\nstderr\n"
assert result_mix.stdout == 'stdout\nstderr\n' assert result_mix.stdout == "stdout\nstderr\n"
with pytest.raises(ValueError): with pytest.raises(ValueError):
result_mix.stderr result_mix.stderr
@pytest.mark.parametrize('args, expected_output', [
(None, 'bar\n'),
([], 'bar\n'),
('', 'bar\n'),
(['--foo', 'one two'], 'one two\n'),
('--foo "one two"', 'one two\n'),
])
def test_args(args, expected_output):
@click.command() @click.command()
@click.option('--foo', default='bar') def cli_empty_stderr():
click.echo("stdout")
runner = CliRunner(mix_stderr=False)
result = runner.invoke(cli_empty_stderr)
assert result.output == "stdout\n"
assert result.stdout == "stdout\n"
assert result.stderr == ""
@pytest.mark.parametrize(
"args, expected_output",
[
(None, "bar\n"),
([], "bar\n"),
("", "bar\n"),
(["--foo", "one two"], "one two\n"),
('--foo "one two"', "one two\n"),
],
)
def test_args(args, expected_output):
@click.command()
@click.option("--foo", default="bar")
def cli_args(foo): def cli_args(foo):
click.echo(foo) click.echo(foo)
@ -286,4 +300,4 @@ def test_setting_prog_name_in_extra():
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, prog_name="foobar") result = runner.invoke(cli, prog_name="foobar")
assert not result.exception assert not result.exception
assert result.output == 'ok\n' assert result.output == "ok\n"

View file

@ -1,173 +1,187 @@
import os import os
import stat
import sys import sys
import pytest import pytest
import click
import click.utils
import click._termui_impl import click._termui_impl
from click._compat import WIN, PY2 import click.utils
from click._compat import WIN
def test_echo(runner): def test_echo(runner):
with runner.isolation() as outstreams: with runner.isolation() as outstreams:
click.echo(u'\N{SNOWMAN}') click.echo(u"\N{SNOWMAN}")
click.echo(b'\x44\x44') click.echo(b"\x44\x44")
click.echo(42, nl=False) click.echo(42, nl=False)
click.echo(b'a', nl=False) click.echo(b"a", nl=False)
click.echo('\x1b[31mx\x1b[39m', nl=False) click.echo("\x1b[31mx\x1b[39m", nl=False)
bytes = outstreams[0].getvalue().replace(b'\r\n', b'\n') bytes = outstreams[0].getvalue().replace(b"\r\n", b"\n")
assert bytes == b'\xe2\x98\x83\nDD\n42ax' assert bytes == b"\xe2\x98\x83\nDD\n42ax"
# If we are in Python 2, we expect that writing bytes into a string io # If we are in Python 2, we expect that writing bytes into a string io
# does not do anything crazy. In Python 3 # does not do anything crazy. In Python 3
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
import StringIO import StringIO
sys.stdout = x = StringIO.StringIO() sys.stdout = x = StringIO.StringIO()
try: try:
click.echo('\xf6') click.echo("\xf6")
finally: finally:
sys.stdout = sys.__stdout__ sys.stdout = sys.__stdout__
assert x.getvalue() == '\xf6\n' assert x.getvalue() == "\xf6\n"
# And in any case, if wrapped, we expect bytes to survive. # And in any case, if wrapped, we expect bytes to survive.
@click.command() @click.command()
def cli(): def cli():
click.echo(b'\xf6') click.echo(b"\xf6")
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.stdout_bytes == b'\xf6\n' assert result.stdout_bytes == b"\xf6\n"
# Ensure we do not strip for bytes. # Ensure we do not strip for bytes.
with runner.isolation() as outstreams: with runner.isolation() as outstreams:
click.echo(bytearray(b'\x1b[31mx\x1b[39m'), nl=False) click.echo(bytearray(b"\x1b[31mx\x1b[39m"), nl=False)
assert outstreams[0].getvalue() == b'\x1b[31mx\x1b[39m' assert outstreams[0].getvalue() == b"\x1b[31mx\x1b[39m"
def test_echo_custom_file(): def test_echo_custom_file():
import io import io
f = io.StringIO() f = io.StringIO()
click.echo(u'hello', file=f) click.echo(u"hello", file=f)
assert f.getvalue() == u'hello\n' assert f.getvalue() == u"hello\n"
def test_styling(): @pytest.mark.parametrize(
examples = [ ("styles", "ref"),
('x', dict(fg='black'), '\x1b[30mx\x1b[0m'), [
('x', dict(fg='red'), '\x1b[31mx\x1b[0m'), ({"fg": "black"}, "\x1b[30mx y\x1b[0m"),
('x', dict(fg='green'), '\x1b[32mx\x1b[0m'), ({"fg": "red"}, "\x1b[31mx y\x1b[0m"),
('x', dict(fg='yellow'), '\x1b[33mx\x1b[0m'), ({"fg": "green"}, "\x1b[32mx y\x1b[0m"),
('x', dict(fg='blue'), '\x1b[34mx\x1b[0m'), ({"fg": "yellow"}, "\x1b[33mx y\x1b[0m"),
('x', dict(fg='magenta'), '\x1b[35mx\x1b[0m'), ({"fg": "blue"}, "\x1b[34mx y\x1b[0m"),
('x', dict(fg='cyan'), '\x1b[36mx\x1b[0m'), ({"fg": "magenta"}, "\x1b[35mx y\x1b[0m"),
('x', dict(fg='white'), '\x1b[37mx\x1b[0m'), ({"fg": "cyan"}, "\x1b[36mx y\x1b[0m"),
('x', dict(bg='black'), '\x1b[40mx\x1b[0m'), ({"fg": "white"}, "\x1b[37mx y\x1b[0m"),
('x', dict(bg='red'), '\x1b[41mx\x1b[0m'), ({"bg": "black"}, "\x1b[40mx y\x1b[0m"),
('x', dict(bg='green'), '\x1b[42mx\x1b[0m'), ({"bg": "red"}, "\x1b[41mx y\x1b[0m"),
('x', dict(bg='yellow'), '\x1b[43mx\x1b[0m'), ({"bg": "green"}, "\x1b[42mx y\x1b[0m"),
('x', dict(bg='blue'), '\x1b[44mx\x1b[0m'), ({"bg": "yellow"}, "\x1b[43mx y\x1b[0m"),
('x', dict(bg='magenta'), '\x1b[45mx\x1b[0m'), ({"bg": "blue"}, "\x1b[44mx y\x1b[0m"),
('x', dict(bg='cyan'), '\x1b[46mx\x1b[0m'), ({"bg": "magenta"}, "\x1b[45mx y\x1b[0m"),
('x', dict(bg='white'), '\x1b[47mx\x1b[0m'), ({"bg": "cyan"}, "\x1b[46mx y\x1b[0m"),
('foo bar', dict(blink=True), '\x1b[5mfoo bar\x1b[0m'), ({"bg": "white"}, "\x1b[47mx y\x1b[0m"),
('foo bar', dict(underline=True), '\x1b[4mfoo bar\x1b[0m'), ({"blink": True}, "\x1b[5mx y\x1b[0m"),
('foo bar', dict(bold=True), '\x1b[1mfoo bar\x1b[0m'), ({"underline": True}, "\x1b[4mx y\x1b[0m"),
('foo bar', dict(dim=True), '\x1b[2mfoo bar\x1b[0m'), ({"bold": True}, "\x1b[1mx y\x1b[0m"),
] ({"dim": True}, "\x1b[2mx y\x1b[0m"),
for text, styles, ref in examples: ],
assert click.style(text, **styles) == ref )
assert click.unstyle(ref) == text def test_styling(styles, ref):
assert click.style("x y", **styles) == ref
assert click.unstyle(ref) == "x y"
@pytest.mark.parametrize(("text", "expect"), [("\x1b[?25lx y\x1b[?25h", "x y")])
def test_unstyle_other_ansi(text, expect):
assert click.unstyle(text) == expect
def test_filename_formatting(): def test_filename_formatting():
assert click.format_filename(b'foo.txt') == 'foo.txt' assert click.format_filename(b"foo.txt") == "foo.txt"
assert click.format_filename(b'/x/foo.txt') == '/x/foo.txt' assert click.format_filename(b"/x/foo.txt") == "/x/foo.txt"
assert click.format_filename(u'/x/foo.txt') == '/x/foo.txt' assert click.format_filename(u"/x/foo.txt") == "/x/foo.txt"
assert click.format_filename(u'/x/foo.txt', shorten=True) == 'foo.txt' assert click.format_filename(u"/x/foo.txt", shorten=True) == "foo.txt"
# filesystem encoding on windows permits this. # filesystem encoding on windows permits this.
if not WIN: if not WIN:
assert click.format_filename(b'/x/foo\xff.txt', shorten=True) \ assert (
== u'foo\ufffd.txt' click.format_filename(b"/x/foo\xff.txt", shorten=True) == u"foo\ufffd.txt"
)
def test_prompts(runner): def test_prompts(runner):
@click.command() @click.command()
def test(): def test():
if click.confirm('Foo'): if click.confirm("Foo"):
click.echo('yes!') click.echo("yes!")
else: else:
click.echo('no :(') click.echo("no :(")
result = runner.invoke(test, input='y\n') result = runner.invoke(test, input="y\n")
assert not result.exception assert not result.exception
assert result.output == 'Foo [y/N]: y\nyes!\n' assert result.output == "Foo [y/N]: y\nyes!\n"
result = runner.invoke(test, input='\n') result = runner.invoke(test, input="\n")
assert not result.exception assert not result.exception
assert result.output == 'Foo [y/N]: \nno :(\n' assert result.output == "Foo [y/N]: \nno :(\n"
result = runner.invoke(test, input='n\n') result = runner.invoke(test, input="n\n")
assert not result.exception assert not result.exception
assert result.output == 'Foo [y/N]: n\nno :(\n' assert result.output == "Foo [y/N]: n\nno :(\n"
@click.command() @click.command()
def test_no(): def test_no():
if click.confirm('Foo', default=True): if click.confirm("Foo", default=True):
click.echo('yes!') click.echo("yes!")
else: else:
click.echo('no :(') click.echo("no :(")
result = runner.invoke(test_no, input='y\n') result = runner.invoke(test_no, input="y\n")
assert not result.exception assert not result.exception
assert result.output == 'Foo [Y/n]: y\nyes!\n' assert result.output == "Foo [Y/n]: y\nyes!\n"
result = runner.invoke(test_no, input='\n') result = runner.invoke(test_no, input="\n")
assert not result.exception assert not result.exception
assert result.output == 'Foo [Y/n]: \nyes!\n' assert result.output == "Foo [Y/n]: \nyes!\n"
result = runner.invoke(test_no, input='n\n') result = runner.invoke(test_no, input="n\n")
assert not result.exception assert not result.exception
assert result.output == 'Foo [Y/n]: n\nno :(\n' assert result.output == "Foo [Y/n]: n\nno :(\n"
@pytest.mark.skipif(WIN, reason='Different behavior on windows.') @pytest.mark.skipif(WIN, reason="Different behavior on windows.")
def test_prompts_abort(monkeypatch, capsys): def test_prompts_abort(monkeypatch, capsys):
def f(_): def f(_):
raise KeyboardInterrupt() raise KeyboardInterrupt()
monkeypatch.setattr('click.termui.hidden_prompt_func', f) monkeypatch.setattr("click.termui.hidden_prompt_func", f)
try: try:
click.prompt('Password', hide_input=True) click.prompt("Password", hide_input=True)
except click.Abort: except click.Abort:
click.echo('Screw you.') click.echo("Screw you.")
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert out == 'Password: \nScrew you.\n' assert out == "Password: \nScrew you.\n"
def _test_gen_func(): def _test_gen_func():
yield 'a' yield "a"
yield 'b' yield "b"
yield 'c' yield "c"
yield 'abc' yield "abc"
@pytest.mark.skipif(WIN, reason='Different behavior on windows.') @pytest.mark.skipif(WIN, reason="Different behavior on windows.")
@pytest.mark.parametrize('cat', ['cat', 'cat ', 'cat ']) @pytest.mark.parametrize("cat", ["cat", "cat ", "cat "])
@pytest.mark.parametrize('test', [ @pytest.mark.parametrize(
# We need lambda here, because pytest will "test",
# reuse the parameters, and then the generators [
# are already used and will not yield anymore # We need lambda here, because pytest will
('just text\n', lambda: 'just text'), # reuse the parameters, and then the generators
('iterable\n', lambda: ["itera", "ble"]), # are already used and will not yield anymore
('abcabc\n', lambda: _test_gen_func), ("just text\n", lambda: "just text"),
('abcabc\n', lambda: _test_gen_func()), ("iterable\n", lambda: ["itera", "ble"]),
('012345\n', lambda: (c for c in range(6))), ("abcabc\n", lambda: _test_gen_func),
]) ("abcabc\n", lambda: _test_gen_func()),
("012345\n", lambda: (c for c in range(6))),
],
)
def test_echo_via_pager(monkeypatch, capfd, cat, test): def test_echo_via_pager(monkeypatch, capfd, cat, test):
monkeypatch.setitem(os.environ, 'PAGER', cat) monkeypatch.setitem(os.environ, "PAGER", cat)
monkeypatch.setattr(click._termui_impl, 'isatty', lambda x: True) monkeypatch.setattr(click._termui_impl, "isatty", lambda x: True)
expected_output = test[0] expected_output = test[0]
test_input = test[1]() test_input = test[1]()
@ -178,35 +192,35 @@ def test_echo_via_pager(monkeypatch, capfd, cat, test):
assert out == expected_output assert out == expected_output
@pytest.mark.skipif(WIN, reason='Test does not make sense on Windows.') @pytest.mark.skipif(WIN, reason="Test does not make sense on Windows.")
def test_echo_color_flag(monkeypatch, capfd): def test_echo_color_flag(monkeypatch, capfd):
isatty = True isatty = True
monkeypatch.setattr(click._compat, 'isatty', lambda x: isatty) monkeypatch.setattr(click._compat, "isatty", lambda x: isatty)
text = 'foo' text = "foo"
styled_text = click.style(text, fg='red') styled_text = click.style(text, fg="red")
assert styled_text == '\x1b[31mfoo\x1b[0m' assert styled_text == "\x1b[31mfoo\x1b[0m"
click.echo(styled_text, color=False) click.echo(styled_text, color=False)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == text + '\n' assert out == "{}\n".format(text)
click.echo(styled_text, color=True) click.echo(styled_text, color=True)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == styled_text + '\n' assert out == "{}\n".format(styled_text)
isatty = True isatty = True
click.echo(styled_text) click.echo(styled_text)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == styled_text + '\n' assert out == "{}\n".format(styled_text)
isatty = False isatty = False
click.echo(styled_text) click.echo(styled_text)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == text + '\n' assert out == "{}\n".format(text)
@pytest.mark.skipif(WIN, reason='Test too complex to make work windows.') @pytest.mark.skipif(WIN, reason="Test too complex to make work windows.")
def test_echo_writing_to_standard_error(capfd, monkeypatch): def test_echo_writing_to_standard_error(capfd, monkeypatch):
def emulate_input(text): def emulate_input(text):
"""Emulate keyboard input.""" """Emulate keyboard input."""
@ -214,92 +228,167 @@ def test_echo_writing_to_standard_error(capfd, monkeypatch):
from StringIO import StringIO from StringIO import StringIO
else: else:
from io import StringIO from io import StringIO
monkeypatch.setattr(sys, 'stdin', StringIO(text)) monkeypatch.setattr(sys, "stdin", StringIO(text))
click.echo('Echo to standard output') click.echo("Echo to standard output")
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == 'Echo to standard output\n' assert out == "Echo to standard output\n"
assert err == '' assert err == ""
click.echo('Echo to standard error', err=True) click.echo("Echo to standard error", err=True)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == '' assert out == ""
assert err == 'Echo to standard error\n' assert err == "Echo to standard error\n"
emulate_input('asdlkj\n') emulate_input("asdlkj\n")
click.prompt('Prompt to stdin') click.prompt("Prompt to stdin")
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == 'Prompt to stdin: ' assert out == "Prompt to stdin: "
assert err == '' assert err == ""
emulate_input('asdlkj\n') emulate_input("asdlkj\n")
click.prompt('Prompt to stderr', err=True) click.prompt("Prompt to stderr", err=True)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == '' assert out == ""
assert err == 'Prompt to stderr: ' assert err == "Prompt to stderr: "
emulate_input('y\n') emulate_input("y\n")
click.confirm('Prompt to stdin') click.confirm("Prompt to stdin")
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == 'Prompt to stdin [y/N]: ' assert out == "Prompt to stdin [y/N]: "
assert err == '' assert err == ""
emulate_input('y\n') emulate_input("y\n")
click.confirm('Prompt to stderr', err=True) click.confirm("Prompt to stderr", err=True)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == '' assert out == ""
assert err == 'Prompt to stderr [y/N]: ' assert err == "Prompt to stderr [y/N]: "
monkeypatch.setattr(click.termui, 'isatty', lambda x: True) monkeypatch.setattr(click.termui, "isatty", lambda x: True)
monkeypatch.setattr(click.termui, 'getchar', lambda: ' ') monkeypatch.setattr(click.termui, "getchar", lambda: " ")
click.pause('Pause to stdout') click.pause("Pause to stdout")
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == 'Pause to stdout\n' assert out == "Pause to stdout\n"
assert err == '' assert err == ""
click.pause('Pause to stderr', err=True) click.pause("Pause to stderr", err=True)
out, err = capfd.readouterr() out, err = capfd.readouterr()
assert out == '' assert out == ""
assert err == 'Pause to stderr\n' assert err == "Pause to stderr\n"
def test_open_file(runner): def test_open_file(runner):
@click.command()
@click.argument("filename")
def cli(filename):
with click.open_file(filename) as f:
click.echo(f.read())
click.echo("meep")
with runner.isolated_filesystem(): with runner.isolated_filesystem():
with open('hello.txt', 'w') as f: with open("hello.txt", "w") as f:
f.write('Cool stuff') f.write("Cool stuff")
result = runner.invoke(cli, ["hello.txt"])
assert result.exception is None
assert result.output == "Cool stuff\nmeep\n"
result = runner.invoke(cli, ["-"], input="foobar")
assert result.exception is None
assert result.output == "foobar\nmeep\n"
def test_open_file_ignore_errors_stdin(runner):
@click.command()
@click.argument("filename")
def cli(filename):
with click.open_file(filename, errors="ignore") as f:
click.echo(f.read())
result = runner.invoke(cli, ["-"], input=os.urandom(16))
assert result.exception is None
def test_open_file_respects_ignore(runner):
with runner.isolated_filesystem():
with open("test.txt", "w") as f:
f.write("Hello world!")
with click.open_file("test.txt", encoding="utf8", errors="ignore") as f:
assert f.errors == "ignore"
def test_open_file_ignore_invalid_utf8(runner):
with runner.isolated_filesystem():
with open("test.txt", "wb") as f:
f.write(b"\xe2\x28\xa1")
with click.open_file("test.txt", encoding="utf8", errors="ignore") as f:
f.read()
def test_open_file_ignore_no_encoding(runner):
with runner.isolated_filesystem():
with open("test.bin", "wb") as f:
f.write(os.urandom(16))
with click.open_file("test.bin", errors="ignore") as f:
f.read()
@pytest.mark.skipif(WIN, reason="os.chmod() is not fully supported on Windows.")
@pytest.mark.parametrize("permissions", [0o400, 0o444, 0o600, 0o644])
def test_open_file_atomic_permissions_existing_file(runner, permissions):
with runner.isolated_filesystem():
with open("existing.txt", "w") as f:
f.write("content")
os.chmod("existing.txt", permissions)
@click.command() @click.command()
@click.argument('filename') @click.argument("filename")
def cli(filename): def cli(filename):
with click.open_file(filename) as f: click.open_file(filename, "w", atomic=True).close()
click.echo(f.read())
click.echo('meep')
result = runner.invoke(cli, ['hello.txt']) result = runner.invoke(cli, ["existing.txt"])
assert result.exception is None assert result.exception is None
assert result.output == 'Cool stuff\nmeep\n' assert stat.S_IMODE(os.stat("existing.txt").st_mode) == permissions
result = runner.invoke(cli, ['-'], input='foobar')
@pytest.mark.skipif(WIN, reason="os.stat() is not fully supported on Windows.")
def test_open_file_atomic_permissions_new_file(runner):
with runner.isolated_filesystem():
@click.command()
@click.argument("filename")
def cli(filename):
click.open_file(filename, "w", atomic=True).close()
# Create a test file to get the expected permissions for new files
# according to the current umask.
with open("test.txt", "w"):
pass
permissions = stat.S_IMODE(os.stat("test.txt").st_mode)
result = runner.invoke(cli, ["new.txt"])
assert result.exception is None assert result.exception is None
assert result.output == 'foobar\nmeep\n' assert stat.S_IMODE(os.stat("new.txt").st_mode) == permissions
@pytest.mark.xfail(WIN and not PY2, reason='God knows ...')
def test_iter_keepopenfile(tmpdir): def test_iter_keepopenfile(tmpdir):
expected = list(map(str, range(10))) expected = list(map(str, range(10)))
p = tmpdir.mkdir('testdir').join('testfile') p = tmpdir.mkdir("testdir").join("testfile")
p.write(os.linesep.join(expected)) p.write("\n".join(expected))
with p.open() as f: 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, click.utils.KeepOpenFile(f)):
assert e_line == a_line.strip() assert e_line == a_line.strip()
@pytest.mark.xfail(WIN and not PY2, reason='God knows ...')
def test_iter_lazyfile(tmpdir): def test_iter_lazyfile(tmpdir):
expected = list(map(str, range(10))) expected = list(map(str, range(10)))
p = tmpdir.mkdir('testdir').join('testfile') p = tmpdir.mkdir("testdir").join("testfile")
p.write(os.linesep.join(expected)) p.write("\n".join(expected))
with p.open() as f: with p.open() as f:
with click.utils.LazyFile(f.name) as lf: with click.utils.LazyFile(f.name) as lf:
for e_line, a_line in zip(expected, lf): for e_line, a_line in zip(expected, lf):

38
tox.ini
View file

@ -1,39 +1,21 @@
[tox] [tox]
envlist = envlist =
py{37,36,35,34,27,py3,py} py{38,37,36,35,27,py3,py}
docs-html style
coverage-report docs
skip_missing_interpreters = true skip_missing_interpreters = true
[testenv] [testenv]
passenv = LANG
deps = deps =
pytest pytest
coverage
colorama colorama
commands = coverage run -p -m pytest {posargs} commands = pytest --tb=short --basetemp={envtmpdir} {posargs}
[testenv:docs-html] [testenv:style]
deps = pre-commit
skip_install = true
commands = pre-commit run --all-files --show-diff-on-failure
[testenv:docs]
deps = -r docs/requirements.txt deps = -r docs/requirements.txt
commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
[testenv:docs-linkcheck]
deps = -r docs/requirements.txt
commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs {envtmpdir}/linkcheck
[testenv:coverage-report]
deps = coverage
skip_install = true
commands =
coverage combine
coverage report
coverage html
[testenv:codecov]
passenv = CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_*
deps = codecov
skip_install = true
commands =
coverage combine
coverage report
codecov