diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..1201593 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,28 @@ +environment: + global: + TOXENV: py,codecov + + matrix: + - PYTHON: C:\Python36-x64 + - PYTHON: C:\Python27-x64 + - PYTHON: C:\Python36 + - PYTHON: C:\Python27 + +init: + - SET PATH=%PYTHON%;%PATH% + +install: + - python -m pip install -U tox + +build: false + +test_script: + - python -m tox + +branches: + only: + - master + - /^.*-maintenance$/ + +cache: + - '%LOCALAPPDATA%\pip\Cache' diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..2876fb5 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +branch = true +source = click,tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe562f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +*.pyc +*.pyo +*.egg-ignore +*.egg-info +.pytest_cache +dist +build +docs/_build +click.egg-info +venv/ +.tox +.cache +.ropeproject +.idea diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..55deb45 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,56 @@ +os: linux +sudo: false +language: python + +matrix: + include: + - python: 3.6 + env: TOXENV=py,codecov + - python: 3.5 + env: TOXENV=py,codecov + - python: 3.4 + env: TOXENV=py,codecov + - python: 2.7 + env: TOXENV=py,codecov + - python: pypy3 + env: TOXENV=py,codecov + - python: nightly + env: TOXENV=py + - os: osx + language: generic + env: TOXENV=py3,py2,codecov + cache: + pip: false + directories: + - $HOME/Library/Caches/Homebrew + - $HOME/Library/Caches/pip + allow_failures: + - python: pypy3 + - python: nightly + - os: osx + fast_finish: true + +before_install: + - | + if [[ $TRAVIS_OS_NAME == 'osx' ]]; then + brew upgrade python + brew install python@2 + export PATH="/usr/local/opt/python/libexec/bin:${PATH}" + fi + +install: + - pip install tox + +script: + - tox + +cache: + - pip + +branches: + only: + - master + - /^.*-maintenance/ + +notifications: + email: false diff --git a/CHANGES b/CHANGES deleted file mode 100644 index 07b1694..0000000 --- a/CHANGES +++ /dev/null @@ -1,351 +0,0 @@ -Click Changelog -=============== - -This contains all major version changes between Click releases. - -Version 6.7 ------------ - -(bugfix release; released on January 6th 2017) - -- 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 ------------ - -(bugfix release; released on April 4th 2016) - -- Fix bug in `click.Path` where it would crash when passed a `-`. See #551. - -Version 6.4 ------------ - -(bugfix release; released on March 24th 2016) - -- Fix bug in bash completion where click would discard one or more trailing - arguments. See #471. - -Version 6.3 ------------ - -(bugfix release; released on February 22 2016) - -- 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 ------------ - -(bugfix release, released on November 27th 2015) - -- Correct fix for hidden progress bars. - -Version 6.1 ------------ - -(bugfix release, released on November 27th 2015) - -- 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 ------------ - -(codename "pow pow", released on November 24th 2015) - -- 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 ------------ - -(bugfix release, released on 17th August 2015) - -- Fix a bug in `pass_obj` that would accidentally pass the context too. - -Version 5.0 ------------ - -(codename "tok tok", released on 16th August 2015) - -- 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 ------------ - -(bugfix release, released on July 14th 2015) - -- 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 ------------ - -(codename "zoom zoom", released on March 31st 2015) - -- 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 ------------ - -(bugfix release, released on September 8th 2014) - -- Fixed an issue with error reporting on Python 3 for invalid forwarding - of commands. - -Version 3.2 ------------ - -(bugfix release, released on August 22nd 2014) - -- 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 ------------ - -(bugfix release, released on August 13th 2014) - -- 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 ------------ - -(codename "clonk clonk", released on August 12th 2014) - -- 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 ------------ - -(bugfix release, released on August 11th 2014) - -- Fixed an issue where the wrapped streams on Python 3 would be reporting - incorrect values for seekable. - -Version 2.5 ------------ - -(bugfix release, released on July 28th 2014) - -- Fixed a bug with text wrapping on Python 3. - -Version 2.4 ------------ - -(bugfix release, released on July 4th 2014) - -- Corrected a bug in the change of the help option in 2.3. - -Version 2.3 ------------ - -(bugfix release, released on July 3rd 2014) - -- 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 ------------ - -(bugfix release, released on June 26th 2014) - -- fixed tty detection on PyPy. -- fixed an issue that progress bars were not rendered when the - context manager was entered. - -Version 2.1 ------------ - -(bugfix release, released on June 14th 2014) - -- 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 ------------ - -(codename "tap tap tap", released on June 6th 2014) - -- 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 ------------ - -(bugfix release, released on May 23rd 2014) - -- fixed a bug that caused text files in Python 2 to not accept - native strings. - -Version 1.0 ------------ - -(no codename, released on May 21st 2014) - -- Initial release. diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..8eaee82 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,558 @@ +Click Changelog +=============== + +This contains all major version changes between Click releases. + +Version 7.0 +----------- + +(upcoming release with new features, release date to be decided) + +- Non-standalone calls to Context.exit return the exit code, rather than + calling ``sys.exit`` (`#667`_)(`#533`_) +- Updated test env matrix. (`#1027`_) +- Fixes a ``ZeroDivisionError`` in ``ProgressBar.make_step``, + when the arg passed to the first call of ``ProgressBar.update`` is 0. (`#1012`_)(`#447`_) +- Document that options can be ``required=True``. (`#1022`_)(`#514`_) +- Fix path validation bug. (`#1020`_)(`#795`_) +- Document customizing option names. (`#1016`_)(`#725`_) +- Wrap ``click.Choice``'s missing message. (`#1000`_)(`#202`_) +- Don't add newlines by default for progressbars. (`#1013`_) +- Document how ``auto_envar_prefix`` works with command groups. (`#1011`_) +- Fix failing bash completion function test signature. +- Clarify how paramteres are named. (`#1009`_)(`#949`_) +- Document bytestripping behavior of ``CliRunner``. (`#1010`_)(`#334`_) +- Fix Google App Engine ``ImportError``. (`#995`_) +- Document that ANSI color info isn't parsedfrom bytearrays in Python 2. (`#334`_) +- Add note to documentation on how parameters are named. +- Fix formatting for short help. (`#1008`_) +- Extract bar formatting to its own method. (`#414`_) +- Move ``fcntl`` import. (`#965`_) +- Fixed issues where ``fd`` was undefined. (`#1007`_) +- Added deprecation flag to commands. (`#1005`_) +- Fix various Sphinx errors. (`#883`_) +- Add ``case_sensitive=False`` as an option to Choice types. (`#887`_) +- Add details about Python version support. (`#1004`_) +- Clarify documentation on command line options. (`#1003`_)(`#741`_) +- Add ``case_sensitive=False`` as an option to Choice. (`#569`_) +- Better handling of help text for dynamic default option values. (`#996`_) +- Allow short width to address cmd formatting. (`#1002`_) +- Add test case checking for custom param type. (`#1001`_) +- Make ``Argument.make_metavar()`` default to type metavar. (`#675`_) +- Show progressbar only if total execution time is visible. (`#487`_) +- Allow setting ``prog_name`` as extra in ``CliRunner.invoke`` (`#999`_)(`#616`_) +- Add support for Sphinx 1.7+ (`#991`_) +- Fix ``get_winter_size()`` so it correctly returns ``(0,0)``. (`#997`_) +- Update progress after iteration. (`#706`_)(`#651`_) +- Add ``show_envvar`` for showing environment variables in help. (`#710`_) +- Add support for bright colors. (`#809`_) +- Add documentation for ``ignore_unkown_options``. (`#684`_) +- Allow ``CliRunner`` to separate stdout and stderr. (`#868`_) +- Implement streaming pager. (`#889`_)(`#409`_) +- Progress bar now uses stderr by default. (`#863`_) +- Do not set options twice. (`#962`_) +- Add Py2/ unicode / str compatability for doc tools. (`#993`_)(`#719`_) +- Add copy option attrs so that custom classes can be re-used. (`#994`_)(`#926`_) +- ``param_hint`` in errors now derived from param itself. (`#709`_)(`#704`_)(`#598`_) +- Add a test that ensures that when an Argument is formatted into a usage error, + its metavar is used, not its name. (`#612`_) +- Fix variable precedence. (`#874`_)(`#873`_) +- Fix ``ResourceWarning`` that occurs during some tests. (`#878`_) +- Update README to match flask style and add ``long_description`` to setup.py. (`#990`_) +- Drop testing for 2.6 3.3 and 3.6. +- Make locale optional (`#880`_) +- Fix invalid escape sequences. (`#877`_) +- Added workaround for jupyter. (`#918`_) +- x and a filemodes now use stdout when file is ``'-'``. (`#929`_) +- ``_AtomicFile`` now uses the realpath of the original filename. (`#920`_) +- Fix missing comma in ``__all__`` list (`#935`_) +- Raw strings added so correct escaping occurs. (`#807`_) +- Add bool conversion for ``t`` and ``f``. (`#842`_) +- Update doc to match arg name for ``path_type``. (`#801`_) +- Add bright colors support for ``click.style`` + and fix the reset option for parameters ``fg`` and ``bg``. (`#703`_) +- Add test and documentation for ``Option`` naming: functionality. (`#799`_) +- Use deterministic option name; can't rely on list sort. (`#794`_)(`#793`_) +- Added support for bash completions containing spaces. (`#773`_) +- Added support for dynamic bash completion from a user-supplied callback. + (`#755`_) +- Added support for bash completion of ``type=click.Choice`` for ``Options`` and + ``Arguments``. (`#535`_) +- The user is now presented with the available choices if ``prompt=True`` and + ``type=click.Choice`` in a ``click.option``. The choices are listed within + parenthesis like ``'Choose fruit (apple, orange): '``. +- The exception objects now store unicode properly. +- Added the ability to hide commands and options from help. +- Added Float Range in Types. +- ``secho``'s first argument can now be ``None``, like in ``echo``. +- Usage errors now hint at the ``--help`` option. +- ``launch`` now works properly under Cygwin. (`#650`_) +- ``CliRunner.invoke`` now may receive ``args`` as a string representing + a Unix shell command. See (`#664`_). +- Fix bug that caused bashcompletion to give improper completions on + chained commands. (`#774`_) +- Add support for bright colors. +- ``'t'`` and ``'f'`` are now converted to ``True`` and ``False``. +- Fix bug that caused bashcompletion to give improper completions on + chained commands when a required option/argument was being completed. + (`#790`_)(`#806`_) +- Allow autocompletion function to determine whether or not to return + completions that start with the incomplete argument. +- Add native ZSH autocompletion support. (`#323`_)(`#865`_) +- Add support for auto-completion documentation. See (`#866`_)(`#869`_) +- Subcommands that are named by the function now automatically have the + underscore replaced with a dash. So if you register a function named + ``my_command`` it becomes ``my-command`` in the command line interface. +- Stdout is now automatically set to non blocking. +- Use realpath to convert atomic file internally into its full canonical + path so that changing working directories does not harm it. +- Force stdout/stderr writable. This works around issues with badly patched + standard streams like those from jupyter. + +.. _#1027: https://github.com/pallets/click/pull/1027 +.. _#1012: https://github.com/pallets/click/pull/1012 +.. _#447: https://github.com/pallets/click/issues/447 +.. _#1022: https://github.com/pallets/click/pull/1022 +.. _#869: https://github.com/pallets/click/pull/869 +.. _#866: https://github.com/pallets/click/issues/866 +.. _#514: https://github.com/pallets/click/issues/514 +.. _#1020: https://github.com/pallets/click/pull/1020 +.. _#795: https://github.com/pallets/click/issues/795 +.. _#1016: https://github.com/pallets/click/pull/1016 +.. _#725: https://github.com/pallets/click/issues/725 +.. _#1000: https://github.com/pallets/click/pull/1000 +.. _#202: https://github.com/pallets/click/issues/202 +.. _#1013: https://github.com/pallets/click/pull/1013 +.. _#1011: https://github.com/pallets/click/pull/1011 +.. _#865: https://github.com/pallets/click/pull/865 +.. _#323: https://github.com/pallets/click/issues/323 +.. _#1009: https://github.com/pallets/click/pull/1009 +.. _#949: https://github.com/pallets/click/issues/949 +.. _#1010: https://github.com/pallets/click/pull/1010 +.. _#334: https://github.com/pallets/click/issues/334 +.. _#995: https://github.com/pallets/click/pull/995 +.. _#1008: https://github.com/pallets/click/pull/1008 +.. _#414: https://github.com/pallets/click/pull/414 +.. _#965: https://github.com/pallets/click/pull/965 +.. _#1005: https://github.com/pallets/click/pull/1005 +.. _#883: https://github.com/pallets/click/pull/883 +.. _#887: https://github.com/pallets/click/pull/887 +.. _#1004: https://github.com/pallets/click/pull/1004 +.. _#1003: https://github.com/pallets/click/pull/1003 +.. _#741: https://github.com/pallets/click/issues/741 +.. _#569: https://github.com/pallets/click/pull/569 +.. _#1007: https://github.com/pallets/click/pull/1007 +.. _#996: https://github.com/pallets/click/pull/996 +.. _#1002: https://github.com/pallets/click/pull/1002 +.. _#1001: https://github.com/pallets/click/pull/1001 +.. _#675: https://github.com/pallets/click/pull/675 +.. _#487: https://github.com/pallets/click/pull/487 +.. _#999: https://github.com/pallets/click/pull/999 +.. _#616: https://github.com/pallets/click/issues/616 +.. _#991: https://github.com/pallets/click/pull/991 +.. _#997: https://github.com/pallets/click/pull/997 +.. _#706: https://github.com/pallets/click/pull/706 +.. _#651: https://github.com/pallets/click/issues/651 +.. _#710: https://github.com/pallets/click/pull/710 +.. _#809: https://github.com/pallets/click/pull/809 +.. _#868: https://github.com/pallets/click/pull/868 +.. _#889: https://github.com/pallets/click/pull/889 +.. _#409: https://github.com/pallets/click/issues/409 +.. _#863: https://github.com/pallets/click/pull/863 +.. _#962: https://github.com/pallets/click/pull/962 +.. _#993: https://github.com/pallets/click/pull/993 +.. _#994: https://github.com/pallets/click/pull/994 +.. _#926: https://github.com/pallets/click/issues/926 +.. _#709: https://github.com/pallets/click/pull/709 +.. _#612: https://github.com/pallets/click/pull/612 +.. _#704: https://github.com/pallets/click/issues/704 +.. _#598: https://github.com/pallets/click/issues/598 +.. _#719: https://github.com/pallets/click/issues/719 +.. _#874: https://github.com/pallets/click/pull/874 +.. _#873: https://github.com/pallets/click/issues/873 +.. _#990: https://github.com/pallets/click/pull/990 +.. _#684: https://github.com/pallets/click/pull/684 +.. _#878: https://github.com/pallets/click/pull/878 +.. _#880: https://github.com/pallets/click/issues/880 +.. _#877: https://github.com/pallets/click/pull/877 +.. _#918: https://github.com/pallets/click/pull/918 +.. _#929: https://github.com/pallets/click/pull/929 +.. _#920: https://github.com/pallets/click/pull/920 +.. _#935: https://github.com/pallets/click/pull/935 +.. _#807: https://github.com/pallets/click/pull/807 +.. _#806: https://github.com/pallets/click/pull/806 +.. _#842: https://github.com/pallets/click/pull/842 +.. _#801: https://github.com/pallets/click/pull/801 +.. _#703: https://github.com/pallets/click/issues/703 +.. _#799: https://github.com/pallets/click/pull/799 +.. _#794: https://github.com/pallets/click/pull/794 +.. _#793: https://github.com/pallets/click/issues/793 +.. _#773: https://github.com/pallets/click/pull/773 +.. _#755: https://github.com/pallets/click/pull/755 +.. _#535: https://github.com/pallets/click/pull/535 +.. _#650: https://github.com/pallets/click/pull/650 +.. _#664: https://github.com/pallets/click/pull/664 +.. _#774: https://github.com/pallets/click/pull/774 +.. _#790: https://github.com/pallets/click/pull/790 + + +Version 6.8 +----------- + +(bugfix release; yet to be released) + +- Disabled ``sys._getframes()`` on Python interpreters that don't support it. See + #728. +- Fix bug in test runner when calling ``sys.exit`` with ``None``. See #739. +- Fix crash on Windows console, see #744. +- Fix bashcompletion on chained commands. See #754. +- Fix option naming routine to match documentation. See #793 +- Fixed the behavior of click error messages with regards to unicode on 2.x + and 3.x respectively. Message is now always unicode and the str and unicode + special methods work as you expect on that platform. + +Version 6.7 +----------- + +(bugfix release; released on January 6th 2017) + +- 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 +----------- + +(bugfix release; released on April 4th 2016) + +- Fix bug in ``click.Path`` where it would crash when passed a ``-``. See #551. + +Version 6.4 +----------- + +(bugfix release; released on March 24th 2016) + +- Fix bug in bash completion where click would discard one or more trailing + arguments. See #471. + +Version 6.3 +----------- + +(bugfix release; released on February 22 2016) + +- 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 +----------- + +(bugfix release, released on November 27th 2015) + +- Correct fix for hidden progress bars. + +Version 6.1 +----------- + +(bugfix release, released on November 27th 2015) + +- 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 +----------- + +(codename "pow pow", released on November 24th 2015) + +- 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 +----------- + +(bugfix release, released on 17th August 2015) + +- Fix a bug in ``pass_obj`` that would accidentally pass the context too. + +Version 5.0 +----------- + +(codename "tok tok", released on 16th August 2015) + +- 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 +----------- + +(bugfix release, released on July 14th 2015) + +- 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 +----------- + +(codename "zoom zoom", released on March 31st 2015) + +- 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 +----------- + +(bugfix release, released on September 8th 2014) + +- Fixed an issue with error reporting on Python 3 for invalid forwarding + of commands. + +Version 3.2 +----------- + +(bugfix release, released on August 22nd 2014) + +- 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 +----------- + +(bugfix release, released on August 13th 2014) + +- 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 +----------- + +(codename "clonk clonk", released on August 12th 2014) + +- 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 +----------- + +(bugfix release, released on August 11th 2014) + +- Fixed an issue where the wrapped streams on Python 3 would be reporting + incorrect values for seekable. + +Version 2.5 +----------- + +(bugfix release, released on July 28th 2014) + +- Fixed a bug with text wrapping on Python 3. + +Version 2.4 +----------- + +(bugfix release, released on July 4th 2014) + +- Corrected a bug in the change of the help option in 2.3. + +Version 2.3 +----------- + +(bugfix release, released on July 3rd 2014) + +- 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 +----------- + +(bugfix release, released on June 26th 2014) + +- fixed tty detection on PyPy. +- fixed an issue that progress bars were not rendered when the + context manager was entered. + +Version 2.1 +----------- + +(bugfix release, released on June 14th 2014) + +- 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 +----------- + +(codename "tap tap tap", released on June 6th 2014) + +- 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 +----------- + +(bugfix release, released on May 23rd 2014) + +- fixed a bug that caused text files in Python 2 to not accept + native strings. + +Version 1.0 +----------- + +(no codename, released on May 21st 2014) + +- Initial release. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..99b493a --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,61 @@ +========================== +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 `_ can help with your issue. If your problem +is not strictly Click-specific, ``#python`` on Freenode is generally more +active. `StackOverflow `_ 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 `_, 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 `_ + +- For bugfixes: Submit against the latest maintenance branch instead of master! + +Running the testsuite +--------------------- + +You probably want to set up a `virtualenv +`_. + +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. diff --git a/MANIFEST.in b/MANIFEST.in index 69c14eb..15458cf 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,12 +1,9 @@ include Makefile CHANGES LICENSE -recursive-include artwork * -recursive-include tests * -recursive-include examples * -recursive-include docs * -recursive-exclude docs *.pyc -recursive-exclude docs *.pyo -recursive-exclude tests *.pyc -recursive-exclude tests *.pyo -recursive-exclude examples *.pyc -recursive-exclude examples *.pyo + +graft artwork +graft tests +graft examples +graft docs prune docs/_build + +global-exclude *.py[co] .DS_Store diff --git a/Makefile b/Makefile index 6927e4c..971e401 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ test: - @cd tests; PYTHONPATH=.. py.test --tb=short + @cd tests; PYTHONPATH=.. pytest --tb=short upload-docs: $(MAKE) -C docs dirhtml diff --git a/PKG-INFO b/PKG-INFO deleted file mode 100644 index bbb17b3..0000000 --- a/PKG-INFO +++ /dev/null @@ -1,13 +0,0 @@ -Metadata-Version: 1.1 -Name: click -Version: 6.7 -Summary: A simple wrapper around optparse for powerful command line utilities. -Home-page: http://github.com/mitsuhiko/click -Author: Armin Ronacher -Author-email: armin.ronacher@active-4.com -License: UNKNOWN -Description: UNKNOWN -Platform: UNKNOWN -Classifier: License :: OSI Approved :: BSD License -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 diff --git a/README b/README deleted file mode 100644 index 02e65f7..0000000 --- a/README +++ /dev/null @@ -1,20 +0,0 @@ -$ click_ - - Click is a Python package for creating beautiful command line interfaces - in a composable way with as little code as necessary. It's the "Command - Line Interface Creation Kit". It's highly configurable but comes with - sensible defaults out of the box. - - It aims to make the process of writing command line tools quick and fun - while also preventing any frustration caused by the inability to implement - an intended CLI API. - - Click in three points: - - - arbitrary nesting of commands - - automatic help page generation - - supports lazy loading of subcommands at runtime - - Read the docs at http://click.pocoo.org/ - - This library is stable and active. Feedback is always welcome! diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b6dd4b1 --- /dev/null +++ b/README.rst @@ -0,0 +1,91 @@ +\$ click\_ +========== + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- arbitrary nesting of commands +- automatic help page generation +- supports lazy loading of subcommands at runtime + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install click + +Click supports Python 3.4 and newer, Python 2.7, and PyPy. + +.. _pip: https://pip.pypa.io/en/stable/quickstart/ + + +A Simple Example +---------------- + +What does it look like? Here is an example of a simple Click program: + +.. code-block:: python + + import click + + @click.command() + @click.option('--count', default=1, help='Number of greetings.') + @click.option('--name', prompt='Your name', + help='The person to greet.') + def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for x in range(count): + click.echo('Hello %s!' % name) + + if __name__ == '__main__': + hello() + +And what it looks like when run: + +.. code-block:: text + + $ python hello.py --count=3 + Your name: John + Hello John! + Hello John! + Hello John! + + +Donate +------ + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +* Website: https://palletsprojects.com/p/click/ +* Documentation: https://click.palletsprojects.com/ +* License: `BSD `_ +* Releases: https://pypi.org/project/click/ +* Code: https://github.com/pallets/click +* Issue tracker: https://github.com/pallets/click/issues +* Test status: + + * Linux, Mac: https://travis-ci.org/pallets/click + * Windows: https://ci.appveyor.com/project/pallets/click + +* Test coverage: https://codecov.io/gh/pallets/click diff --git a/click.egg-info/PKG-INFO b/click.egg-info/PKG-INFO deleted file mode 100644 index bbb17b3..0000000 --- a/click.egg-info/PKG-INFO +++ /dev/null @@ -1,13 +0,0 @@ -Metadata-Version: 1.1 -Name: click -Version: 6.7 -Summary: A simple wrapper around optparse for powerful command line utilities. -Home-page: http://github.com/mitsuhiko/click -Author: Armin Ronacher -Author-email: armin.ronacher@active-4.com -License: UNKNOWN -Description: UNKNOWN -Platform: UNKNOWN -Classifier: License :: OSI Approved :: BSD License -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 diff --git a/click.egg-info/SOURCES.txt b/click.egg-info/SOURCES.txt deleted file mode 100644 index 5eed434..0000000 --- a/click.egg-info/SOURCES.txt +++ /dev/null @@ -1,114 +0,0 @@ -CHANGES -LICENSE -MANIFEST.in -Makefile -README -setup.cfg -setup.py -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 -click.egg-info/PKG-INFO -click.egg-info/SOURCES.txt -click.egg-info/dependency_links.txt -click.egg-info/top_level.txt -docs/Makefile -docs/advanced.rst -docs/api.rst -docs/arguments.rst -docs/bashcomplete.rst -docs/changelog.rst -docs/clickdoctools.py -docs/commands.rst -docs/complex.rst -docs/conf.py -docs/contrib.rst -docs/documentation.rst -docs/exceptions.rst -docs/index.rst -docs/license.rst -docs/make.bat -docs/options.rst -docs/parameters.rst -docs/prompts.rst -docs/python3.rst -docs/quickstart.rst -docs/setuptools.rst -docs/testing.rst -docs/upgrading.rst -docs/utils.rst -docs/why.rst -docs/wincmd.rst -docs/_static/click-small.png -docs/_static/click-small@2x.png -docs/_static/click.png -docs/_static/click@2x.png -docs/_templates/sidebarintro.html -docs/_templates/sidebarlogo.html -examples/README -examples/aliases/README -examples/aliases/aliases.ini -examples/aliases/aliases.py -examples/aliases/setup.py -examples/colors/README -examples/colors/colors.py -examples/colors/setup.py -examples/complex/README -examples/complex/setup.py -examples/complex/complex/__init__.py -examples/complex/complex/cli.py -examples/complex/complex/commands/__init__.py -examples/complex/complex/commands/cmd_init.py -examples/complex/complex/commands/cmd_status.py -examples/imagepipe/.gitignore -examples/imagepipe/README -examples/imagepipe/example01.jpg -examples/imagepipe/example02.jpg -examples/imagepipe/imagepipe.py -examples/imagepipe/setup.py -examples/inout/README -examples/inout/inout.py -examples/inout/setup.py -examples/naval/README -examples/naval/naval.py -examples/naval/setup.py -examples/repo/README -examples/repo/repo.py -examples/repo/setup.py -examples/termui/README -examples/termui/setup.py -examples/termui/termui.py -examples/validation/README -examples/validation/setup.py -examples/validation/validation.py -tests/conftest.py -tests/test_arguments.py -tests/test_bashcomplete.py -tests/test_basic.py -tests/test_chain.py -tests/test_commands.py -tests/test_compat.py -tests/test_context.py -tests/test_defaults.py -tests/test_formatting.py -tests/test_imports.py -tests/test_normalization.py -tests/test_options.py -tests/test_termui.py -tests/test_testing.py -tests/test_utils.py \ No newline at end of file diff --git a/click.egg-info/dependency_links.txt b/click.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/click.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/click.egg-info/top_level.txt b/click.egg-info/top_level.txt deleted file mode 100644 index dca9a90..0000000 --- a/click.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -click diff --git a/click/__init__.py b/click/__init__.py index 971e55d..6de314b 100644 --- a/click/__init__.py +++ b/click/__init__.py @@ -28,7 +28,7 @@ from .decorators import pass_context, pass_obj, make_pass_decorator, \ # Types from .types import ParamType, File, Path, Choice, IntRange, Tuple, \ - STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED + STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED, FloatRange # Utilities from .utils import echo, get_binary_stream, get_text_stream, open_file, \ @@ -66,7 +66,7 @@ __all__ = [ # Types 'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'Tuple', 'STRING', - 'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED', + 'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED', 'FloatRange', # Utilities 'echo', 'get_binary_stream', 'get_text_stream', 'open_file', @@ -95,4 +95,4 @@ __all__ = [ disable_unicode_literals_warning = False -__version__ = '6.7' +__version__ = '7.0-dev' diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py index d9d26d2..db42865 100644 --- a/click/_bashcomplete.py +++ b/click/_bashcomplete.py @@ -1,27 +1,63 @@ +import collections +import copy import os import re + from .utils import echo from .parser import split_arg_string -from .core import MultiCommand, Option +from .core import MultiCommand, Option, Argument +from .types import Choice +WORDBREAK = '=' -COMPLETION_SCRIPT = ''' +COMPLETION_SCRIPT_BASH = ''' %(complete_func)s() { + local IFS=$'\n' COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ COMP_CWORD=$COMP_CWORD \\ %(autocomplete_var)s=complete $1 ) ) return 0 } -complete -F %(complete_func)s -o default %(script_names)s +complete -F %(complete_func)s %(script_names)s +''' + +COMPLETION_SCRIPT_ZSH = ''' +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\ + COMP_CWORD=$((CURRENT-1)) \\ + %(autocomplete_var)s=\"complete_zsh\" \\ + %(script_names)s )}") + + for key descr in ${(kv)response}; do + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe '' completions_with_descriptions + fi + if [ -n "$completions" ]; then + compadd -M 'r:|=* l:|=* r:|=*' -a completions + fi +} + +compdef %(complete_func)s %(script_names)s ''' _invalid_ident_char_re = re.compile(r'[^a-zA-Z0-9_]') -def get_completion_script(prog_name, complete_var): +def get_completion_script(prog_name, complete_var, shell): cf_name = _invalid_ident_char_re.sub('', prog_name.replace('-', '_')) - return (COMPLETION_SCRIPT % { + script = COMPLETION_SCRIPT_ZSH if shell == 'zsh' else COMPLETION_SCRIPT_BASH + return (script % { 'complete_func': '_%s_completion' % cf_name, 'script_names': prog_name, 'autocomplete_var': complete_var, @@ -29,37 +65,169 @@ def get_completion_script(prog_name, complete_var): def resolve_ctx(cli, prog_name, args): + """ + Parse into a hierarchy of contexts. Contexts are connected through the parent variable. + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :return: the final context/command parsed + """ ctx = cli.make_context(prog_name, args, resilient_parsing=True) - while ctx.protected_args + ctx.args and isinstance(ctx.command, MultiCommand): - a = ctx.protected_args + ctx.args - cmd = ctx.command.get_command(ctx, a[0]) - if cmd is None: - return None - ctx = cmd.make_context(a[0], a[1:], parent=ctx, resilient_parsing=True) + args_remaining = ctx.protected_args + ctx.args + while ctx is not None and args_remaining: + if isinstance(ctx.command, MultiCommand): + cmd = ctx.command.get_command(ctx, args_remaining[0]) + if cmd is None: + return None + ctx = cmd.make_context( + args_remaining[0], args_remaining[1:], parent=ctx, resilient_parsing=True) + args_remaining = ctx.protected_args + ctx.args + else: + ctx = ctx.parent + return ctx +def start_of_option(param_str): + """ + :param param_str: param_str to check + :return: whether or not this is the start of an option declaration (i.e. starts "-" or "--") + """ + return param_str and param_str[:1] == '-' + + +def is_incomplete_option(all_args, cmd_param): + """ + :param all_args: the full original list of args supplied + :param cmd_param: the current command paramter + :return: whether or not the last option declaration (i.e. starts "-" or "--") is incomplete and + corresponds to this cmd_param. In other words whether this cmd_param option can still accept + values + """ + if not isinstance(cmd_param, Option): + return False + if cmd_param.is_flag: + return False + last_option = None + for index, arg_str in enumerate(reversed([arg for arg in all_args if arg != WORDBREAK])): + if index + 1 > cmd_param.nargs: + break + if start_of_option(arg_str): + last_option = arg_str + + return True if last_option and last_option in cmd_param.opts else False + + +def is_incomplete_argument(current_params, cmd_param): + """ + :param current_params: the current params and values for this argument as already entered + :param cmd_param: the current command parameter + :return: whether or not the last argument is incomplete and corresponds to this cmd_param. In + other words whether or not the this cmd_param argument can still accept values + """ + if not isinstance(cmd_param, Argument): + return False + current_param_values = current_params[cmd_param.name] + if current_param_values is None: + return True + if cmd_param.nargs == -1: + return True + if isinstance(current_param_values, collections.Iterable) \ + and cmd_param.nargs > 1 and len(current_param_values) < cmd_param.nargs: + return True + return False + + +def get_user_autocompletions(ctx, args, incomplete, cmd_param): + """ + :param ctx: context associated with the parsed command + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :param cmd_param: command definition + :return: all the possible user-specified completions for the param + """ + results = [] + if isinstance(cmd_param.type, Choice): + # Choices don't support descriptions. + results = [(c, None) + for c in cmd_param.type.choices if c.startswith(incomplete)] + elif cmd_param.autocompletion is not None: + dynamic_completions = cmd_param.autocompletion(ctx=ctx, + args=args, + incomplete=incomplete) + results = [c if isinstance(c, tuple) else (c, None) + for c in dynamic_completions] + return results + + +def add_subcommand_completions(ctx, incomplete, completions_out): + # Add subcommand completions. + if isinstance(ctx.command, MultiCommand): + completions_out.extend( + [(c, ctx.command.get_command(ctx, c).get_short_help_str()) for c in ctx.command.list_commands(ctx) if c.startswith(incomplete)]) + + # Walk up the context list and add any other completion possibilities from chained commands + while ctx.parent is not None: + ctx = ctx.parent + if isinstance(ctx.command, MultiCommand) and ctx.command.chain: + remaining_commands = sorted( + set(ctx.command.list_commands(ctx)) - set(ctx.protected_args)) + completions_out.extend( + [(c, ctx.command.get_command(ctx, c).get_short_help_str()) for c in remaining_commands if c.startswith(incomplete)]) + + def get_choices(cli, prog_name, args, incomplete): + """ + :param cli: command definition + :param prog_name: the program that is running + :param args: full list of args + :param incomplete: the incomplete text to autocomplete + :return: all the possible completions for the incomplete + """ + all_args = copy.deepcopy(args) + ctx = resolve_ctx(cli, prog_name, args) if ctx is None: - return + return [] - choices = [] - if incomplete and not incomplete[:1].isalnum(): + # In newer versions of bash long opts with '='s are partitioned, but it's easier to parse + # without the '=' + if start_of_option(incomplete) and WORDBREAK in incomplete: + partition_incomplete = incomplete.partition(WORDBREAK) + all_args.append(partition_incomplete[0]) + incomplete = partition_incomplete[2] + elif incomplete == WORDBREAK: + incomplete = '' + + completions = [] + if start_of_option(incomplete): + # completions for partial options for param in ctx.command.params: - if not isinstance(param, Option): - continue - choices.extend(param.opts) - choices.extend(param.secondary_opts) - elif isinstance(ctx.command, MultiCommand): - choices.extend(ctx.command.list_commands(ctx)) + if isinstance(param, Option): + param_opts = [param_opt for param_opt in param.opts + + param.secondary_opts if param_opt not in all_args or param.multiple] + completions.extend( + [(o, param.help) for o in param_opts if o.startswith(incomplete)]) + return completions + # completion for option values from user supplied values + for param in ctx.command.params: + if is_incomplete_option(all_args, param): + return get_user_autocompletions(ctx, all_args, incomplete, param) + # completion for argument values from user supplied values + for param in ctx.command.params: + if is_incomplete_argument(ctx.params, param): + completions.extend(get_user_autocompletions( + ctx, all_args, incomplete, param)) + # Stop looking for other completions only if this argument is required. + if param.required: + return completions + break - for item in choices: - if item.startswith(incomplete): - yield item + add_subcommand_completions(ctx, incomplete, completions) + return completions -def do_complete(cli, prog_name): +def do_complete(cli, prog_name, include_descriptions): cwords = split_arg_string(os.environ['COMP_WORDS']) cword = int(os.environ['COMP_CWORD']) args = cwords[1:cword] @@ -69,15 +237,19 @@ def do_complete(cli, prog_name): incomplete = '' for item in get_choices(cli, prog_name, args, incomplete): - echo(item) + echo(item[0]) + 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. + echo(item[1] if item[1] else '_') return True def bashcomplete(cli, prog_name, complete_var, complete_instr): - if complete_instr == 'source': - echo(get_completion_script(prog_name, complete_var)) + if complete_instr.startswith('source'): + shell = 'zsh' if complete_instr == 'source_zsh' else 'bash' + echo(get_completion_script(prog_name, complete_var, shell)) return True - elif complete_instr == 'complete': - return do_complete(cli, prog_name) + elif complete_instr == 'complete' or complete_instr == 'complete_zsh': + return do_complete(cli, prog_name, complete_instr == 'complete_zsh') return False diff --git a/click/_compat.py b/click/_compat.py index 2b43412..e8b06a4 100644 --- a/click/_compat.py +++ b/click/_compat.py @@ -7,24 +7,31 @@ from weakref import WeakKeyDictionary PY2 = sys.version_info[0] == 2 -WIN = sys.platform.startswith('win') +CYGWIN = sys.platform.startswith('cygwin') +# Determine local App Engine environment, per Google's own suggestion +APP_ENGINE = ('APPENGINE_RUNTIME' in os.environ and + 'Development/' in os.environ['SERVER_SOFTWARE']) +WIN = sys.platform.startswith('win') and not APP_ENGINE DEFAULT_COLUMNS = 80 -_ansi_re = re.compile('\033\[((?:\d|;)*)([a-zA-Z])') +_ansi_re = re.compile(r'\033\[((?:\d|;)*)([a-zA-Z])') def get_filesystem_encoding(): return sys.getfilesystemencoding() or sys.getdefaultencoding() -def _make_text_stream(stream, encoding, errors): +def _make_text_stream(stream, encoding, errors, + force_readable=False, force_writable=False): if encoding is None: encoding = get_best_encoding(stream) if errors is None: errors = 'replace' return _NonClosingTextIOWrapper(stream, encoding, errors, - line_buffering=True) + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable) def is_ascii_encoding(encoding): @@ -45,8 +52,10 @@ def get_best_encoding(stream): class _NonClosingTextIOWrapper(io.TextIOWrapper): - def __init__(self, stream, encoding, errors, **extra): - self._stream = stream = _FixupStream(stream) + def __init__(self, stream, encoding, errors, + force_readable=False, force_writable=False, **extra): + self._stream = stream = _FixupStream(stream, force_readable, + force_writable) io.TextIOWrapper.__init__(self, stream, encoding, errors, **extra) # The io module is a place where the Python 3 text behavior @@ -81,10 +90,16 @@ class _FixupStream(object): """The new io interface needs more from streams than streams traditionally implement. As such, this fix-up code is necessary in some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). """ - def __init__(self, stream): + def __init__(self, stream, force_readable=False, force_writable=False): self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable def __getattr__(self, name): return getattr(self._stream, name) @@ -101,6 +116,8 @@ class _FixupStream(object): return self._stream.read(size) def readable(self): + if self._force_readable: + return True x = getattr(self._stream, 'readable', None) if x is not None: return x() @@ -111,6 +128,8 @@ class _FixupStream(object): return True def writable(self): + if self._force_writable: + return True x = getattr(self._stream, 'writable', None) if x is not None: return x() @@ -139,6 +158,7 @@ if PY2: bytes = str raw_input = raw_input string_types = (str, unicode) + int_types = (int, long) iteritems = lambda x: x.iteritems() range_type = xrange @@ -165,10 +185,13 @@ if PY2: # available (which is why we use try-catch instead of the WIN variable # here), such as the Google App Engine development server on Windows. In # those cases there is just nothing we can do. + def set_binary_mode(f): + return f + try: import msvcrt except ImportError: - set_binary_mode = lambda x: x + pass else: def set_binary_mode(f): try: @@ -179,6 +202,21 @@ if PY2: msvcrt.setmode(fileno, os.O_BINARY) return f + try: + import fcntl + except ImportError: + pass + else: + def set_binary_mode(f): + try: + fileno = f.fileno() + except Exception: + pass + else: + flags = fcntl.fcntl(fileno, fcntl.F_GETFL) + fcntl.fcntl(fileno, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + return f + def isidentifier(x): return _identifier_re.search(x) is not None @@ -195,19 +233,22 @@ if PY2: rv = _get_windows_console_stream(sys.stdin, encoding, errors) if rv is not None: return rv - return _make_text_stream(sys.stdin, encoding, errors) + return _make_text_stream(sys.stdin, encoding, errors, + force_readable=True) def get_text_stdout(encoding=None, errors=None): rv = _get_windows_console_stream(sys.stdout, encoding, errors) if rv is not None: return rv - return _make_text_stream(sys.stdout, encoding, errors) + return _make_text_stream(sys.stdout, encoding, errors, + force_writable=True) def get_text_stderr(encoding=None, errors=None): rv = _get_windows_console_stream(sys.stderr, encoding, errors) if rv is not None: return rv - return _make_text_stream(sys.stderr, encoding, errors) + return _make_text_stream(sys.stderr, encoding, errors, + force_writable=True) def filename_to_ui(value): if isinstance(value, bytes): @@ -218,6 +259,7 @@ else: text_type = str raw_input = input string_types = (str,) + int_types = (int,) range_type = range isidentifier = lambda x: x.isidentifier() iteritems = lambda x: iter(x.items()) @@ -298,7 +340,8 @@ else: return False - def _force_correct_text_reader(text_reader, encoding, errors): + def _force_correct_text_reader(text_reader, encoding, errors, + force_readable=False): if _is_binary_reader(text_reader, False): binary_reader = text_reader else: @@ -324,9 +367,11 @@ else: # we're so fundamentally fucked that nothing can repair it. if errors is None: errors = 'replace' - return _make_text_stream(binary_reader, encoding, errors) + return _make_text_stream(binary_reader, encoding, errors, + force_readable=force_readable) - def _force_correct_text_writer(text_writer, encoding, errors): + def _force_correct_text_writer(text_writer, encoding, errors, + force_writable=False): if _is_binary_writer(text_writer, False): binary_writer = text_writer else: @@ -352,7 +397,8 @@ else: # we're so fundamentally fucked that nothing can repair it. if errors is None: errors = 'replace' - return _make_text_stream(binary_writer, encoding, errors) + return _make_text_stream(binary_writer, encoding, errors, + force_writable=force_writable) def get_binary_stdin(): reader = _find_binary_reader(sys.stdin) @@ -379,19 +425,22 @@ else: rv = _get_windows_console_stream(sys.stdin, encoding, errors) if rv is not None: return rv - return _force_correct_text_reader(sys.stdin, encoding, errors) + return _force_correct_text_reader(sys.stdin, encoding, errors, + force_readable=True) def get_text_stdout(encoding=None, errors=None): rv = _get_windows_console_stream(sys.stdout, encoding, errors) if rv is not None: return rv - return _force_correct_text_writer(sys.stdout, encoding, errors) + return _force_correct_text_writer(sys.stdout, encoding, errors, + force_writable=True) def get_text_stderr(encoding=None, errors=None): rv = _get_windows_console_stream(sys.stderr, encoding, errors) if rv is not None: return rv - return _force_correct_text_writer(sys.stderr, encoding, errors) + return _force_correct_text_writer(sys.stderr, encoding, errors, + force_writable=True) def filename_to_ui(value): if isinstance(value, bytes): @@ -420,7 +469,7 @@ def open_stream(filename, mode='r', encoding=None, errors='strict', # Standard streams first. These are simple because they don't need # special handling for the atomic flag. It's entirely ignored. if filename == '-': - if 'w' in mode: + if any(m in mode for m in ['w', 'a', 'x']): if 'b' in mode: return get_binary_stdout(), False return get_text_stdout(encoding=encoding, errors=errors), False @@ -460,7 +509,7 @@ def open_stream(filename, mode='r', encoding=None, errors='strict', else: f = os.fdopen(fd, mode) - return _AtomicFile(f, tmp_filename, filename), True + return _AtomicFile(f, tmp_filename, os.path.realpath(filename)), True # Used in a destructor call, needs extra protection from interpreter cleanup. diff --git a/click/_termui_impl.py b/click/_termui_impl.py index 7cfd3d5..0b39731 100644 --- a/click/_termui_impl.py +++ b/click/_termui_impl.py @@ -13,8 +13,10 @@ import os import sys import time import math +import contextlib from ._compat import _default_text_stdout, range_type, PY2, isatty, \ - open_stream, strip_ansi, term_len, get_best_encoding, WIN + open_stream, strip_ansi, term_len, get_best_encoding, WIN, int_types, \ + CYGWIN from .utils import echo from .exceptions import ClickException @@ -41,7 +43,7 @@ def _length_hint(obj): except TypeError: return None if hint is NotImplemented or \ - not isinstance(hint, (int, long)) or \ + not isinstance(hint, int_types) or \ hint < 0: return None return hint @@ -88,6 +90,7 @@ class ProgressBar(object): self.current_item = None self.is_hidden = not isatty(self.file) self._last_line = None + self.short_limit = 0.5 def __enter__(self): self.entered = True @@ -101,10 +104,13 @@ class ProgressBar(object): if not self.entered: raise RuntimeError('You need to use progress bars in a with block.') self.render_progress() - return self + return self.generator() + + def is_fast(self): + return time.time() - self.start <= self.short_limit def render_finish(self): - if self.is_hidden: + if self.is_hidden or self.is_fast(): return self.file.write(AFTER_BAR) self.file.flush() @@ -129,13 +135,13 @@ class ProgressBar(object): def format_eta(self): if self.eta_known: - t = self.eta + 1 + t = int(self.eta) seconds = t % 60 - t /= 60 + t //= 60 minutes = t % 60 - t /= 60 + t //= 60 hours = t % 24 - t /= 24 + t //= 24 if t > 0: days = t return '%dd %02d:%02d:%02d' % (days, hours, minutes, seconds) @@ -152,25 +158,27 @@ class ProgressBar(object): def format_pct(self): return ('% 4d%%' % int(self.pct * 100))[1:] - def format_progress_line(self): - show_percent = self.show_percent - - info_bits = [] + def format_bar(self): if self.length_known: bar_length = int(self.pct * self.width) bar = self.fill_char * bar_length bar += self.empty_char * (self.width - bar_length) - if show_percent is None: - show_percent = not self.show_pos + elif self.finished: + bar = self.fill_char * self.width else: - if self.finished: - bar = self.fill_char * self.width - else: - bar = list(self.empty_char * (self.width or 1)) - if self.time_per_iteration != 0: - bar[int((math.cos(self.pos * self.time_per_iteration) - / 2.0 + 0.5) * self.width)] = self.fill_char - bar = ''.join(bar) + bar = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + bar[int((math.cos(self.pos * self.time_per_iteration) + / 2.0 + 0.5) * self.width)] = self.fill_char + bar = ''.join(bar) + return bar + + def format_progress_line(self): + show_percent = self.show_percent + + info_bits = [] + if self.length_known and show_percent is None: + show_percent = not self.show_pos if self.show_pos: info_bits.append(self.format_pos()) @@ -185,49 +193,47 @@ class ProgressBar(object): return (self.bar_template % { 'label': self.label, - 'bar': bar, + 'bar': self.format_bar(), 'info': self.info_sep.join(info_bits) }).rstrip() def render_progress(self): from .termui import get_terminal_size - nl = False if self.is_hidden: - buf = [self.label] - nl = True - else: - buf = [] - # Update width in case the terminal has been resized - if self.autowidth: - old_width = self.width - self.width = 0 - clutter_length = term_len(self.format_progress_line()) - new_width = max(0, get_terminal_size()[0] - clutter_length) - if new_width < old_width: - buf.append(BEFORE_BAR) - buf.append(' ' * self.max_width) - self.max_width = new_width - self.width = new_width + return - clear_width = self.width - if self.max_width is not None: - clear_width = self.max_width + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, get_terminal_size()[0] - clutter_length) + if new_width < old_width: + buf.append(BEFORE_BAR) + buf.append(' ' * self.max_width) + self.max_width = new_width + self.width = new_width - buf.append(BEFORE_BAR) - line = self.format_progress_line() - line_len = term_len(line) - if self.max_width is None or self.max_width < line_len: - self.max_width = line_len - buf.append(line) + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width - buf.append(' ' * (clear_width - line_len)) + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(' ' * (clear_width - line_len)) line = ''.join(buf) - # Render the line only if it changed. - if line != self._last_line: + + if line != self._last_line and not self.is_fast(): self._last_line = line - echo(line, file=self.file, color=self.color, nl=nl) + echo(line, file=self.file, color=self.color, nl=False) self.file.flush() def make_step(self, n_steps): @@ -239,7 +245,16 @@ class ProgressBar(object): return self.last_eta = time.time() - self.avg = self.avg[-6:] + [-(self.start - time.time()) / (self.pos)] + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] self.eta_known = self.length_known @@ -252,54 +267,56 @@ class ProgressBar(object): self.current_item = None self.finished = True - def next(self): + def generator(self): + """ + Returns a generator which yields the items added to the bar during + construction, and updates the progress bar *after* the yielded block + returns. + """ + if not self.entered: + raise RuntimeError('You need to use progress bars in a with block.') + if self.is_hidden: - return next(self.iter) - try: - rv = next(self.iter) - self.current_item = rv - except StopIteration: + for rv in self.iter: + yield rv + else: + for rv in self.iter: + self.current_item = rv + yield rv + self.update(1) self.finish() self.render_progress() - raise StopIteration() - else: - self.update(1) - return rv - - if not PY2: - __next__ = next - del next -def pager(text, color=None): +def pager(generator, color=None): """Decide what method to use for paging through text.""" stdout = _default_text_stdout() if not isatty(sys.stdin) or not isatty(stdout): - return _nullpager(stdout, text, color) + return _nullpager(stdout, generator, color) pager_cmd = (os.environ.get('PAGER', None) or '').strip() if pager_cmd: if WIN: - return _tempfilepager(text, pager_cmd, color) - return _pipepager(text, pager_cmd, color) + return _tempfilepager(generator, pager_cmd, color) + return _pipepager(generator, pager_cmd, color) if os.environ.get('TERM') in ('dumb', 'emacs'): - return _nullpager(stdout, text, color) + return _nullpager(stdout, generator, color) if WIN or sys.platform.startswith('os2'): - return _tempfilepager(text, 'more <', color) + return _tempfilepager(generator, 'more <', color) if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0: - return _pipepager(text, 'less', color) + return _pipepager(generator, 'less', color) import tempfile fd, filename = tempfile.mkstemp() os.close(fd) try: if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0: - return _pipepager(text, 'more', color) - return _nullpager(stdout, text, color) + return _pipepager(generator, 'more', color) + return _nullpager(stdout, generator, color) finally: os.unlink(filename) -def _pipepager(text, cmd, color): +def _pipepager(generator, cmd, color): """Page through text by feeding it to another program. Invoking a pager through this might support colors. """ @@ -317,17 +334,19 @@ def _pipepager(text, cmd, color): elif 'r' in less_flags or 'R' in less_flags: color = True - if not color: - text = strip_ansi(text) - c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) encoding = get_best_encoding(c.stdin) try: - c.stdin.write(text.encode(encoding, 'replace')) - c.stdin.close() + for text in generator: + if not color: + text = strip_ansi(text) + + c.stdin.write(text.encode(encoding, 'replace')) except (IOError, KeyboardInterrupt): pass + else: + c.stdin.close() # Less doesn't respect ^C, but catches it for its own UI purposes (aborting # search or other commands inside less). @@ -346,10 +365,12 @@ def _pipepager(text, cmd, color): break -def _tempfilepager(text, cmd, color): +def _tempfilepager(generator, cmd, color): """Page through text by invoking a program on a temporary file.""" import tempfile filename = tempfile.mktemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) if not color: text = strip_ansi(text) encoding = get_best_encoding(sys.stdout) @@ -361,11 +382,12 @@ def _tempfilepager(text, cmd, color): os.unlink(filename) -def _nullpager(stream, text, color): +def _nullpager(stream, generator, color): """Simply print unformatted text. This is the ultimate fallback.""" - if not color: - text = strip_ansi(text) - stream.write(text) + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) class Editor(object): @@ -478,6 +500,14 @@ def open_url(url, wait=False, locate=False): args = 'start %s "" "%s"' % ( wait and '/WAIT' or '', url.replace('"', '')) return os.system(args) + elif CYGWIN: + if locate: + url = _unquote_file(url) + args = 'cygstart "%s"' % (os.path.dirname(url).replace('"', '')) + else: + args = 'cygstart %s "%s"' % ( + wait and '-w' or '', url.replace('"', '')) + return os.system(args) try: if locate: @@ -506,6 +536,10 @@ def _translate_ch_to_exc(ch): if WIN: import msvcrt + @contextlib.contextmanager + def raw_terminal(): + yield + def getchar(echo): rv = msvcrt.getch() if echo: @@ -522,7 +556,8 @@ else: import tty import termios - def getchar(echo): + @contextlib.contextmanager + def raw_terminal(): if not isatty(sys.stdin): f = open('/dev/tty') fd = f.fileno() @@ -533,9 +568,7 @@ else: old_settings = termios.tcgetattr(fd) try: tty.setraw(fd) - ch = os.read(fd, 32) - if echo and isatty(sys.stdout): - sys.stdout.write(ch) + yield fd finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) sys.stdout.flush() @@ -543,5 +576,11 @@ else: f.close() except termios.error: pass - _translate_ch_to_exc(ch) - return ch.decode(get_best_encoding(sys.stdin), 'replace') + + def getchar(echo): + with raw_terminal() as fd: + ch = os.read(fd, 32) + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + _translate_ch_to_exc(ch) + return ch.decode(get_best_encoding(sys.stdin), 'replace') diff --git a/click/_unicodefun.py b/click/_unicodefun.py index 9e17a38..6383415 100644 --- a/click/_unicodefun.py +++ b/click/_unicodefun.py @@ -14,6 +14,8 @@ 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: @@ -60,8 +62,11 @@ def _verify_python3_env(): extra = '' if os.name == 'posix': import subprocess - rv = subprocess.Popen(['locale', '-a'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE).communicate()[0] + 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 @@ -94,7 +99,7 @@ def _verify_python3_env(): else: extra += ( 'This system lists a couple of UTF-8 supporting locales that\n' - 'you can pick from. The following suitable locales where\n' + 'you can pick from. The following suitable locales were\n' 'discovered: %s' ) % ', '.join(sorted(good_locales)) @@ -114,5 +119,5 @@ def _verify_python3_env(): raise RuntimeError('Click will abort further execution because Python 3 ' 'was configured to use ASCII as encoding for the ' - 'environment. Consult http://click.pocoo.org/python3/' + 'environment. Consult http://click.pocoo.org/python3/ ' 'for mitigation steps.' + extra) diff --git a/click/_winconsole.py b/click/_winconsole.py index 9aed942..f4d95dd 100644 --- a/click/_winconsole.py +++ b/click/_winconsole.py @@ -15,7 +15,7 @@ import zlib import time import ctypes import msvcrt -from click._compat import _NonClosingTextIOWrapper, text_type, PY2 +from ._compat import _NonClosingTextIOWrapper, text_type, PY2 from ctypes import byref, POINTER, c_int, c_char, c_char_p, \ c_void_p, py_object, c_ssize_t, c_ulong, windll, WINFUNCTYPE try: @@ -261,7 +261,7 @@ def _get_windows_console_stream(f, encoding, errors): func = _stream_factories.get(f.fileno()) if func is not None: if not PY2: - f = getattr(f, 'buffer') + f = getattr(f, 'buffer', None) if f is None: return None else: diff --git a/click/core.py b/click/core.py index 7456451..30aec62 100644 --- a/click/core.py +++ b/click/core.py @@ -1,4 +1,5 @@ import errno +import inspect import os import sys from contextlib import contextmanager @@ -8,13 +9,13 @@ from functools import update_wrapper from .types import convert_type, IntRange, BOOL from .utils import make_str, make_default_short_help, echo, get_os_args from .exceptions import ClickException, UsageError, BadParameter, Abort, \ - MissingParameter -from .termui import prompt, confirm + MissingParameter, Exit +from .termui import prompt, confirm, style from .formatting import HelpFormatter, join_options from .parser import OptionParser, split_opt from .globals import push_context, pop_context -from ._compat import PY2, isidentifier, iteritems +from ._compat import PY2, isidentifier, iteritems, string_types from ._unicodefun import _check_for_unicode_literals, _verify_python3_env @@ -24,6 +25,24 @@ _missing = object() SUBCOMMAND_METAVAR = 'COMMAND [ARGS]...' SUBCOMMANDS_METAVAR = 'COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...' +DEPRECATED_HELP_NOTICE = ' (DEPRECATED)' +DEPRECATED_INVOKE_NOTICE = 'DeprecationWarning: ' + \ + 'The command %(name)s is deprecated.' + + +def _maybe_show_deprecated_notice(cmd): + if cmd.deprecated: + echo(style(DEPRECATED_INVOKE_NOTICE % {'name': cmd.name}, fg='red'), err=True) + + +def fast_exit(code): + """Exit without garbage collection, this speeds up exit by about 10ms for + things like bash completion. + """ + sys.stdout.flush() + sys.stderr.flush() + os._exit(code) + def _bashcomplete(cmd, prog_name, complete_var=None): """Internal handler for the bash completion support.""" @@ -35,7 +54,7 @@ def _bashcomplete(cmd, prog_name, complete_var=None): from ._bashcomplete import bashcomplete if bashcomplete(cmd, prog_name, complete_var, complete_instr): - sys.exit(1) + fast_exit(1) def _check_multicommand(base_command, cmd_name, cmd, register=False): @@ -50,9 +69,7 @@ def _check_multicommand(base_command, cmd_name, cmd, register=False): raise RuntimeError('%s. Command "%s" is set to chain and "%s" was ' 'added as subcommand but it in itself is a ' 'multi command. ("%s" is a %s within a chained ' - '%s named "%s"). This restriction was supposed to ' - 'be lifted in 6.0 but the fix was flawed. This ' - 'will be fixed in Click 7.0' % ( + '%s named "%s").' % ( hint, base_command.name, cmd_name, cmd_name, cmd.__class__.__name__, base_command.__class__.__name__, @@ -372,7 +389,7 @@ class Context(object): @property def meta(self): """This is a dictionary which is shared with all the contexts - that are nested. It exists so that click utiltiies can store some + that are nested. It exists so that click utilities can store some state here if they need to. It is however the responsibility of that code to manage this dictionary well. @@ -481,7 +498,7 @@ class Context(object): def exit(self, code=0): """Exits the application with a given exit code.""" - sys.exit(code) + raise Exit(code) def get_usage(self): """Helper method to get formatted usage string for the current @@ -655,7 +672,7 @@ class BaseCommand(object): name from ``sys.argv[0]``. :param complete_var: the environment variable that controls the bash completion support. The default is - ``"__COMPLETE"`` with prog name in + ``"__COMPLETE"`` with prog_name in uppercase. :param standalone_mode: the default behavior is to invoke the script in standalone mode. Click will then @@ -670,7 +687,7 @@ class BaseCommand(object): constructor. See :class:`Context` for more information. """ # If we are in Python 3, we will verify that the environment is - # sane at this point of reject further execution to avoid a + # sane at this point or reject further execution to avoid a # broken script. if not PY2: _verify_python3_env() @@ -697,6 +714,13 @@ class BaseCommand(object): rv = self.invoke(ctx) if not standalone_mode: return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness ctx.exit() except (EOFError, KeyboardInterrupt): echo(file=sys.stderr) @@ -711,6 +735,19 @@ class BaseCommand(object): sys.exit(1) else: raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code except Abort: if not standalone_mode: raise @@ -743,11 +780,16 @@ class Command(BaseCommand): shown on the command listing of the parent command. :param add_help_option: by default each command registers a ``--help`` option. This can be disabled by this parameter. + :param hidden: hide this command from help outputs. + + :param deprecated: issues a message indicating that + the command is deprecated. """ def __init__(self, name, context_settings=None, callback=None, params=None, help=None, epilog=None, short_help=None, - options_metavar='[OPTIONS]', add_help_option=True): + options_metavar='[OPTIONS]', add_help_option=True, + hidden=False, deprecated=False): BaseCommand.__init__(self, name, context_settings) #: the callback to execute when the command fires. This might be #: `None` in which case nothing happens. @@ -756,13 +798,17 @@ class Command(BaseCommand): #: should show up in the help page and execute. Eager parameters #: will automatically be handled before non eager ones. self.params = params or [] + # if a form feed (page break) is found in the help text, truncate help + # text to the content preceding the first form feed + if help and '\f' in help: + help = help.split('\f', 1)[0] self.help = help self.epilog = epilog self.options_metavar = options_metavar - if short_help is None and help: - short_help = make_default_short_help(help) self.short_help = short_help self.add_help_option = add_help_option + self.hidden = hidden + self.deprecated = deprecated def get_usage(self, ctx): formatter = ctx.make_formatter() @@ -816,8 +862,6 @@ class Command(BaseCommand): def make_parser(self, ctx): """Creates the underlying option parser for this command.""" parser = OptionParser(ctx) - parser.allow_interspersed_args = ctx.allow_interspersed_args - parser.ignore_unknown_options = ctx.ignore_unknown_options for param in self.get_params(ctx): param.add_to_parser(parser, ctx) return parser @@ -830,6 +874,10 @@ class Command(BaseCommand): self.format_help(ctx, formatter) return formatter.getvalue().rstrip('\n') + def get_short_help_str(self, limit=45): + """Gets short help for the command or makes it by shortening the long help string.""" + return self.short_help or self.help and make_default_short_help(self.help, limit) or '' + def format_help(self, ctx, formatter): """Writes the help into the formatter if it exists. @@ -850,7 +898,14 @@ class Command(BaseCommand): if self.help: formatter.write_paragraph() with formatter.indentation(): - formatter.write_text(self.help) + help_text = self.help + if self.deprecated: + help_text += DEPRECATED_HELP_NOTICE + formatter.write_text(help_text) + elif self.deprecated: + formatter.write_paragraph() + with formatter.indentation(): + formatter.write_text(DEPRECATED_HELP_NOTICE) def format_options(self, ctx, formatter): """Writes all the options into the formatter if they exist.""" @@ -891,6 +946,7 @@ class Command(BaseCommand): """Given a context, this invokes the attached callback (if it exists) in the right way. """ + _maybe_show_deprecated_notice(self) if self.callback is not None: return ctx.invoke(self.callback, **ctx.params) @@ -996,19 +1052,29 @@ class MultiCommand(Command): """Extra format methods for multi methods that adds all the commands after the options. """ - rows = [] + commands = [] for subcommand in self.list_commands(ctx): cmd = self.get_command(ctx, subcommand) # What is this, the tool lied about a command. Ignore it if cmd is None: continue + if cmd.hidden: + continue - help = cmd.short_help or '' - rows.append((subcommand, help)) + commands.append((subcommand, cmd)) - if rows: - with formatter.section('Commands'): - formatter.write_dl(rows) + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section('Commands'): + formatter.write_dl(rows) def parse_args(self, ctx, args): if not args and self.no_args_is_help and not ctx.resilient_parsing: @@ -1216,7 +1282,7 @@ class CommandCollection(MultiCommand): class Parameter(object): - """A parameter to a command comes in two versions: they are either + r"""A parameter to a command comes in two versions: they are either :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently not supported by design as some of the internals for parsing are intentionally not finalized. @@ -1261,7 +1327,8 @@ class Parameter(object): 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): + 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) @@ -1284,6 +1351,7 @@ class Parameter(object): self.is_eager = is_eager self.metavar = metavar self.envvar = envvar + self.autocompletion = autocompletion @property def human_readable_name(self): @@ -1314,12 +1382,13 @@ class Parameter(object): def add_to_parser(self, parser, ctx): pass + def consume_value(self, ctx, opts): value = opts.get(self.name) - if value is None: - value = ctx.lookup_default(self.name) if value is None: value = self.value_from_envvar(ctx) + if value is None: + value = ctx.lookup_default(self.name) return value def type_cast_value(self, ctx, value): @@ -1416,6 +1485,13 @@ class Parameter(object): def get_usage_pieces(self, ctx): return [] + def get_error_hint(self, ctx): + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return ' / '.join('"%s"' % x for x in hint_list) + class Option(Parameter): """Options are usually optional values on the command line and @@ -1424,10 +1500,15 @@ class Option(Parameter): All other parameters are passed onwards to the parameter constructor. :param show_default: controls if the default value should be shown on the - help page. Normally, defaults are not shown. - :param prompt: if set to `True` or a non empty string then the user will - be prompted for input if not set. If set to `True` the - prompt will be the option name capitalized. + help page. Normally, defaults are not shown. If this + value is a string, it shows the string instead of the + value. This is particularly useful for dynamic options. + :param show_envvar: controls if an environment variable should be shown on + the help page. Normally, environment variables + are not shown. + :param prompt: if set to `True` or a non empty string then the user will be + prompted for input. If set to `True` the prompt will be the + option name capitalized. :param confirmation_prompt: if set then the value will need to be confirmed if it was prompted for. :param hide_input: if this is `True` then the input on the prompt will be @@ -1448,6 +1529,7 @@ class Option(Parameter): variable in case a prefix is defined on the context. :param help: the help string. + :param hidden: hide this option from help outputs. """ param_type_name = 'option' @@ -1455,7 +1537,8 @@ class Option(Parameter): 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, **attrs): + 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) @@ -1468,6 +1551,7 @@ class Option(Parameter): self.prompt = prompt_text self.confirmation_prompt = confirmation_prompt self.hide_input = hide_input + self.hidden = hidden # Flags if is_flag is None: @@ -1500,6 +1584,8 @@ class Option(Parameter): self.allow_from_autoenv = allow_from_autoenv self.help = help self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar # Sanity check for stuff we don't support if __debug__: @@ -1548,8 +1634,8 @@ class Option(Parameter): opts.append(decl) if name is None and possible_names: - possible_names.sort(key=lambda x: len(x[0])) - name = possible_names[-1][1].replace('-', '_').lower() + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace('-', '_').lower() if not isidentifier(name): name = None @@ -1595,6 +1681,8 @@ class Option(Parameter): parser.add_option(self.opts, **kwargs) def get_help_record(self, ctx): + if self.hidden: + return any_prefix_is_slash = [] def _write_opts(opts): @@ -1611,11 +1699,28 @@ class Option(Parameter): help = self.help or '' extra = [] + if self.show_envvar: + envvar = self.envvar + if envvar is None: + if self.allow_from_autoenv and \ + ctx.auto_envvar_prefix is not None: + envvar = '%s_%s' % (ctx.auto_envvar_prefix, self.name.upper()) + if envvar is not None: + extra.append('env var: %s' % ( + ', '.join('%s' % d for d in envvar) + if isinstance(envvar, (list, tuple)) + else envvar, )) if self.default is not None and self.show_default: - extra.append('default: %s' % ( - ', '.join('%s' % d for d in self.default) - if isinstance(self.default, (list, tuple)) - else self.default, )) + if isinstance(self.show_default, string_types): + default_string = '({})'.format(self.show_default) + elif isinstance(self.default, (list, tuple)): + default_string = ', '.join('%s' % d for d in self.default) + elif inspect.isfunction(self.default): + default_string = "(dynamic)" + else: + default_string = self.default + extra.append('default: {}'.format(default_string)) + if self.required: extra.append('required') if extra: @@ -1649,8 +1754,8 @@ class Option(Parameter): if self.is_bool_flag: return confirm(self.prompt, default) - return prompt(self.prompt, default=default, - hide_input=self.hide_input, + return prompt(self.prompt, default=default, 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)) @@ -1710,7 +1815,9 @@ class Argument(Parameter): def make_metavar(self): if self.metavar is not None: return self.metavar - var = self.name.upper() + var = self.type.get_metavar(self) + if not var: + var = self.name.upper() if not self.required: var = '[%s]' % var if self.nargs != 1: @@ -1735,6 +1842,9 @@ class Argument(Parameter): def get_usage_pieces(self, ctx): return [self.make_metavar()] + def get_error_hint(self, ctx): + return '"%s"' % self.make_metavar() + def add_to_parser(self, parser, ctx): parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) diff --git a/click/decorators.py b/click/decorators.py index 9893452..c57c530 100644 --- a/click/decorators.py +++ b/click/decorators.py @@ -61,7 +61,7 @@ def make_pass_decorator(object_type, ensure=False): raise RuntimeError('Managed to invoke callback without a ' 'context object of type %r existing' % object_type.__name__) - return ctx.invoke(f, obj, *args[1:], **kwargs) + return ctx.invoke(f, obj, *args, **kwargs) return update_wrapper(new_func, f) return decorator @@ -85,12 +85,12 @@ def _make_command(f, name, attrs, cls): help = inspect.cleandoc(help) attrs['help'] = help _check_for_unicode_literals() - return cls(name=name or f.__name__.lower(), + return cls(name=name or f.__name__.lower().replace('_', '-'), callback=f, params=params, **attrs) def command(name=None, cls=None, **attrs): - """Creates a new :class:`Command` and uses the decorated function as + r"""Creates a new :class:`Command` and uses the decorated function as callback. This will also automatically attach all decorated :func:`option`\s and :func:`argument`\s as parameters to the command. @@ -105,7 +105,7 @@ def command(name=None, cls=None, **attrs): command :class:`Group`. :param name: the name of the command. This defaults to the function - name. + name with underscores replaced by dashes. :param cls: the command class to instantiate. This defaults to :class:`Command`. """ @@ -164,10 +164,13 @@ def option(*param_decls, **attrs): :class:`Option`. """ def decorator(f): - if 'help' in attrs: - attrs['help'] = inspect.cleandoc(attrs['help']) - OptionClass = attrs.pop('cls', Option) - _param_memo(f, OptionClass(param_decls, **attrs)) + # Issue 926, copy attrs, so pre-defined options can re-use the same cls= + option_attrs = attrs.copy() + + if 'help' in option_attrs: + option_attrs['help'] = inspect.cleandoc(option_attrs['help']) + OptionClass = option_attrs.pop('cls', Option) + _param_memo(f, OptionClass(param_decls, **option_attrs)) return f return decorator @@ -235,7 +238,11 @@ def version_option(version=None, *param_decls, **attrs): :param others: everything else is forwarded to :func:`option`. """ if version is None: - module = sys._getframe(1).f_globals.get('__name__') + if hasattr(sys, '_getframe'): + module = sys._getframe(1).f_globals.get('__name__') + else: + module = '' + def decorator(f): prog_name = attrs.pop('prog_name', None) message = attrs.pop('message', '%(prog)s, version %(version)s') diff --git a/click/exceptions.py b/click/exceptions.py index 74a4542..6fa1765 100644 --- a/click/exceptions.py +++ b/click/exceptions.py @@ -2,6 +2,12 @@ from ._compat import PY2, filename_to_ui, get_text_stderr from .utils import echo +def _join_param_hints(param_hint): + if isinstance(param_hint, (tuple, list)): + return ' / '.join('"%s"' % x for x in param_hint) + return param_hint + + class ClickException(Exception): """An exception that Click can handle and show to the user.""" @@ -9,15 +15,25 @@ class ClickException(Exception): exit_code = 1 def __init__(self, message): + ctor_msg = message if PY2: - if message is not None: - message = message.encode('utf-8') - Exception.__init__(self, message) + if ctor_msg is not None: + ctor_msg = ctor_msg.encode('utf-8') + Exception.__init__(self, ctor_msg) self.message = message def format_message(self): return self.message + def __str__(self): + return self.message + + if PY2: + __unicode__ = __str__ + + def __str__(self): + return self.message.encode('utf-8') + def show(self, file=None): if file is None: file = get_text_stderr() @@ -37,14 +53,20 @@ class UsageError(ClickException): def __init__(self, message, ctx=None): ClickException.__init__(self, message) self.ctx = ctx + self.cmd = self.ctx and self.ctx.command or None def show(self, file=None): if file is None: file = get_text_stderr() color = None + hint = '' + if (self.cmd is not None and + self.cmd.get_help_option(self.ctx) is not None): + hint = ('Try "%s %s" for help.\n' + % (self.ctx.command_path, self.ctx.help_option_names[0])) if self.ctx is not None: color = self.ctx.color - echo(self.ctx.get_usage() + '\n', file=file, color=color) + echo(self.ctx.get_usage() + '\n%s' % hint, file=file, color=color) echo('Error: %s' % self.format_message(), file=file, color=color) @@ -76,11 +98,11 @@ class BadParameter(UsageError): if self.param_hint is not None: param_hint = self.param_hint elif self.param is not None: - param_hint = self.param.opts or [self.param.human_readable_name] + param_hint = self.param.get_error_hint(self.ctx) else: return 'Invalid value: %s' % self.message - if isinstance(param_hint, (tuple, list)): - param_hint = ' / '.join('"%s"' % x for x in param_hint) + param_hint = _join_param_hints(param_hint) + return 'Invalid value for %s: %s' % (param_hint, self.message) @@ -105,11 +127,10 @@ class MissingParameter(BadParameter): if self.param_hint is not None: param_hint = self.param_hint elif self.param is not None: - param_hint = self.param.opts or [self.param.human_readable_name] + param_hint = self.param.get_error_hint(self.ctx) else: param_hint = None - if isinstance(param_hint, (tuple, list)): - param_hint = ' / '.join('"%s"' % x for x in param_hint) + param_hint = _join_param_hints(param_hint) param_type = self.param_type if param_type is None and self.param is not None: @@ -164,10 +185,13 @@ class BadOptionUsage(UsageError): for an option is not correct. .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. """ - def __init__(self, message, ctx=None): + def __init__(self, option_name, message, ctx=None): UsageError.__init__(self, message, ctx) + self.option_name = option_name class BadArgumentUsage(UsageError): @@ -199,3 +223,13 @@ class FileError(ClickException): class Abort(RuntimeError): """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + def __init__(self, code=0): + self.exit_code = code diff --git a/click/globals.py b/click/globals.py index 14338e6..843b594 100644 --- a/click/globals.py +++ b/click/globals.py @@ -9,7 +9,7 @@ def get_current_context(silent=False): access the current context object from anywhere. This is a more implicit alternative to the :func:`pass_context` decorator. This function is primarily useful for helpers such as :func:`echo` which might be - interested in changing it's behavior based on the current context. + interested in changing its behavior based on the current context. To push the current context, :meth:`Context.scope` can be used. diff --git a/click/parser.py b/click/parser.py index 9775c9f..4fe3d2f 100644 --- a/click/parser.py +++ b/click/parser.py @@ -74,8 +74,8 @@ def _unpack_args(args, nargs_spec): def _error_opt_args(nargs, opt): if nargs == 1: - raise BadOptionUsage('%s option requires an argument' % opt) - raise BadOptionUsage('%s option requires %d arguments' % (opt, nargs)) + raise BadOptionUsage(opt, '%s option requires an argument' % opt) + raise BadOptionUsage(opt, '%s option requires %d arguments' % (opt, nargs)) def split_opt(opt): @@ -342,7 +342,7 @@ class OptionParser(object): del state.rargs[:nargs] elif explicit_value is not None: - raise BadOptionUsage('%s option does not take a value' % opt) + raise BadOptionUsage(opt, '%s option does not take a value' % opt) else: value = None diff --git a/click/termui.py b/click/termui.py index d9fba52..70eaceb 100644 --- a/click/termui.py +++ b/click/termui.py @@ -1,12 +1,14 @@ import os import sys import struct +import inspect +import itertools from ._compat import raw_input, text_type, string_types, \ isatty, strip_ansi, get_winterm_size, DEFAULT_COLUMNS, WIN from .utils import echo from .exceptions import Abort, UsageError -from .types import convert_type +from .types import convert_type, Choice, Path from .globals import resolve_color_default @@ -14,8 +16,25 @@ from .globals import resolve_color_default # functions to customize how they work. visible_prompt_func = raw_input -_ansi_colors = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', - 'cyan', 'white', 'reset') +_ansi_colors = { + 'black': 30, + 'red': 31, + 'green': 32, + 'yellow': 33, + 'blue': 34, + 'magenta': 35, + 'cyan': 36, + 'white': 37, + 'reset': 39, + 'bright_black': 90, + 'bright_red': 91, + 'bright_green': 92, + 'bright_yellow': 93, + 'bright_blue': 94, + 'bright_magenta': 95, + 'bright_cyan': 96, + 'bright_white': 97, +} _ansi_reset_all = '\033[0m' @@ -24,23 +43,27 @@ def hidden_prompt_func(prompt): return getpass.getpass(prompt) -def _build_prompt(text, suffix, show_default=False, default=None): +def _build_prompt(text, suffix, show_default=False, default=None, show_choices=True, type=None): prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += ' (' + ", ".join(map(str, type.choices)) + ')' if default is not None and show_default: prompt = '%s [%s]' % (prompt, default) return prompt + suffix -def prompt(text, default=None, hide_input=False, - confirmation_prompt=False, type=None, - value_proc=None, prompt_suffix=': ', - show_default=True, err=False): +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 be used to prompt a user for input later. If the user aborts the input by sending a interrupt signal, this function will catch it and raise a :exc:`Abort` exception. + .. versionadded:: 7.0 + Added the show_choices parameter. + .. versionadded:: 6.0 Added unicode support for cmd.exe on Windows. @@ -61,6 +84,10 @@ def prompt(text, default=None, hide_input=False, :param show_default: shows or hides the default value in the prompt. :param err: if set to true the file defaults to ``stderr`` instead of ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". """ result = None @@ -82,17 +109,18 @@ def prompt(text, default=None, hide_input=False, if value_proc is None: value_proc = convert_type(type, default) - prompt = _build_prompt(text, prompt_suffix, show_default, default) + prompt = _build_prompt(text, prompt_suffix, show_default, default, show_choices, type) while 1: while 1: value = prompt_func(prompt) if value: break - # If a default is set and used, then the confirmation - # prompt is always skipped because that's the only thing - # that really makes sense. elif default is not None: + if isinstance(value_proc, Path): + # validate Path default value(exists, dir_okay etc.) + value = default + break return default try: result = value_proc(value) @@ -166,8 +194,14 @@ def get_terminal_size(): sz = shutil_get_terminal_size() return sz.columns, sz.lines + # We provide a sensible default for get_winterm_size() when being invoked + # inside a subprocess. Without this, it would not provide a useful input. if get_winterm_size is not None: - return get_winterm_size() + size = get_winterm_size() + if size == (0, 0): + return (79, 24) + else: + return size def ioctl_gwinsz(fd): try: @@ -195,22 +229,33 @@ def get_terminal_size(): return int(cr[1]), int(cr[0]) -def echo_via_pager(text, color=None): +def echo_via_pager(text_or_generator, color=None): """This function takes a text and shows it via an environment specific pager on stdout. .. versionchanged:: 3.0 Added the `color` flag. - :param text: the text to page. + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. :param color: controls if the pager supports ANSI colors or not. The default is autodetection. """ color = resolve_color_default(color) - if not isinstance(text, string_types): - text = text_type(text) + + if inspect.isgeneratorfunction(text_or_generator): + i = text_or_generator() + elif isinstance(text_or_generator, string_types): + i = [text_or_generator] + else: + i = iter(text_or_generator) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, string_types) else text_type(el) + for el in i) + from ._termui_impl import pager - return pager(text + '\n', color) + return pager(itertools.chain(text_generator, "\n"), color) def progressbar(iterable=None, length=None, label=None, show_eta=True, @@ -347,10 +392,21 @@ def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, * ``magenta`` * ``cyan`` * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` * ``reset`` (reset the color code only) .. versionadded:: 2.0 + .. versionadded:: 7.0 + Added support for bright colors. + :param text: the string to style with ansi codes. :param fg: if provided this will become the foreground color. :param bg: if provided this will become the background color. @@ -369,13 +425,13 @@ def style(text, fg=None, bg=None, bold=None, dim=None, underline=None, bits = [] if fg: try: - bits.append('\033[%dm' % (_ansi_colors.index(fg) + 30)) - except ValueError: + bits.append('\033[%dm' % (_ansi_colors[fg])) + except KeyError: raise TypeError('Unknown color %r' % fg) if bg: try: - bits.append('\033[%dm' % (_ansi_colors.index(bg) + 40)) - except ValueError: + bits.append('\033[%dm' % (_ansi_colors[bg] + 10)) + except KeyError: raise TypeError('Unknown color %r' % bg) if bold is not None: bits.append('\033[%dm' % (1 if bold else 22)) @@ -405,7 +461,7 @@ def unstyle(text): return strip_ansi(text) -def secho(text, file=None, nl=True, err=False, color=None, **styles): +def secho(message=None, file=None, nl=True, err=False, color=None, **styles): """This function combines :func:`echo` and :func:`style` into one call. As such the following two calls are the same:: @@ -417,7 +473,9 @@ def secho(text, file=None, nl=True, err=False, color=None, **styles): .. versionadded:: 2.0 """ - return echo(style(text, **styles), file=file, nl=nl, err=err, color=color) + if message is not None: + message = style(message, **styles) + return echo(message, file=file, nl=nl, err=err, color=color) def edit(text=None, editor=None, env=None, require_save=True, @@ -510,6 +568,11 @@ def getchar(echo=False): return f(echo) +def raw_terminal(): + from ._termui_impl import raw_terminal as f + return f() + + def pause(info='Press any key to continue ...', err=False): """This command stops execution and waits for the user to press any key to continue. This is similar to the Windows batch "pause" diff --git a/click/testing.py b/click/testing.py index 4416c77..1b2924e 100644 --- a/click/testing.py +++ b/click/testing.py @@ -3,8 +3,9 @@ import sys import shutil import tempfile import contextlib +import shlex -from ._compat import iteritems, PY2 +from ._compat import iteritems, PY2, string_types # If someone wants to vendor click, we want to ensure the @@ -72,27 +73,44 @@ def make_input_stream(input, charset): class Result(object): """Holds the captured result of an invoked CLI script.""" - def __init__(self, runner, output_bytes, exit_code, exception, - exc_info=None): + def __init__(self, runner, stdout_bytes, stderr_bytes, exit_code, + exception, exc_info=None): #: The runner that created the result self.runner = runner - #: The output as bytes. - self.output_bytes = output_bytes + #: The standard output as bytes. + self.stdout_bytes = stdout_bytes + #: The standard error as bytes, or False(y) if not available + self.stderr_bytes = stderr_bytes #: The exit code as integer. self.exit_code = exit_code - #: The exception that happend if one did. + #: The exception that happened if one did. self.exception = exception #: The traceback self.exc_info = exc_info @property def output(self): - """The output as unicode string.""" - return self.output_bytes.decode(self.runner.charset, 'replace') \ + """The (standard) output as unicode string.""" + return self.stdout + + @property + def stdout(self): + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, 'replace') \ .replace('\r\n', '\n') + @property + def stderr(self): + """The standard error as unicode string.""" + if not self.stderr_bytes: + raise ValueError("stderr not separately captured") + return self.stderr_bytes.decode(self.runner.charset, 'replace') \ + .replace('\r\n', '\n') + + def __repr__(self): - return '' % ( + return '<%s %s>' % ( + type(self).__name__, self.exception and repr(self.exception) or 'okay', ) @@ -111,14 +129,21 @@ class CliRunner(object): to stdout. This is useful for showing examples in some circumstances. Note that regular prompts will automatically echo the input. + :param mix_stderr: if this is set to `False`, then stdout and stderr are + preserved as independent streams. This is useful for + Unix-philosophy apps that have predictable stdout and + noisy stderr, such that each may be measured + independently """ - def __init__(self, charset=None, env=None, echo_stdin=False): + def __init__(self, charset=None, env=None, echo_stdin=False, + mix_stderr=True): if charset is None: charset = 'utf-8' self.charset = charset self.env = env or {} self.echo_stdin = echo_stdin + self.mix_stderr = mix_stderr def get_default_prog_name(self, cli): """Given a command object it will return the default program name @@ -163,16 +188,27 @@ class CliRunner(object): env = self.make_env(env) if PY2: - sys.stdout = sys.stderr = bytes_output = StringIO() + bytes_output = StringIO() if self.echo_stdin: input = EchoingStdin(input, bytes_output) + sys.stdout = bytes_output + if not self.mix_stderr: + bytes_error = StringIO() + sys.stderr = bytes_error else: bytes_output = io.BytesIO() if self.echo_stdin: input = EchoingStdin(input, bytes_output) input = io.TextIOWrapper(input, encoding=self.charset) - sys.stdout = sys.stderr = io.TextIOWrapper( + sys.stdout = io.TextIOWrapper( bytes_output, encoding=self.charset) + if not self.mix_stderr: + bytes_error = io.BytesIO() + sys.stderr = io.TextIOWrapper( + bytes_error, encoding=self.charset) + + if self.mix_stderr: + sys.stderr = sys.stdout sys.stdin = input @@ -196,6 +232,7 @@ class CliRunner(object): return char default_color = color + def should_strip_ansi(stream=None, color=None): if color is None: return not default_color @@ -221,7 +258,7 @@ class CliRunner(object): pass else: os.environ[key] = value - yield bytes_output + yield (bytes_output, not self.mix_stderr and bytes_error) finally: for key, value in iteritems(old_env): if value is None: @@ -241,7 +278,7 @@ class CliRunner(object): clickpkg.formatting.FORCED_WIDTH = old_forced_width def invoke(self, cli, args=None, input=None, env=None, - catch_exceptions=True, color=False, **extra): + catch_exceptions=True, color=False, mix_stderr=False, **extra): """Invokes a command in an isolated environment. The arguments are forwarded directly to the command line script, the `extra` keyword arguments are passed to the :meth:`~clickpkg.Command.main` function of @@ -260,7 +297,10 @@ class CliRunner(object): The ``color`` parameter was added. :param cli: the command to invoke - :param args: the arguments to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. :param input: the input data for `sys.stdin`. :param env: the environment overrides. :param catch_exceptions: Whether to catch any other exceptions than @@ -270,36 +310,48 @@ class CliRunner(object): application can still override this explicitly. """ exc_info = None - with self.isolation(input=input, env=env, color=color) as out: + with self.isolation(input=input, env=env, color=color) as outstreams: exception = None exit_code = 0 + if isinstance(args, string_types): + args = shlex.split(args) + try: - cli.main(args=args or (), - prog_name=self.get_default_prog_name(cli), **extra) + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + cli.main(args=args or (), prog_name=prog_name, **extra) except SystemExit as e: - if e.code != 0: + exc_info = sys.exc_info() + exit_code = e.code + if exit_code is None: + exit_code = 0 + + if exit_code != 0: exception = e - exc_info = sys.exc_info() - - exit_code = e.code if not isinstance(exit_code, int): sys.stdout.write(str(exit_code)) sys.stdout.write('\n') exit_code = 1 + except Exception as e: if not catch_exceptions: raise exception = e - exit_code = -1 + exit_code = 1 exc_info = sys.exc_info() finally: sys.stdout.flush() - output = out.getvalue() + stdout = outstreams[0].getvalue() + stderr = outstreams[1] and outstreams[1].getvalue() return Result(runner=self, - output_bytes=output, + stdout_bytes=stdout, + stderr_bytes=stderr, exit_code=exit_code, exception=exception, exc_info=exc_info) diff --git a/click/types.py b/click/types.py index 3639002..0d4b122 100644 --- a/click/types.py +++ b/click/types.py @@ -126,34 +126,54 @@ class StringParamType(ParamType): class Choice(ParamType): - """The choice type allows a value to be checked against a fixed set of - supported values. All of these values have to be strings. + """The choice type allows a value to be checked against a fixed set + of supported values. All of these values have to be strings. + + You should only pass a list or tuple of choices. Other iterables + (like generators) may lead to surprising results. See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. """ + name = 'choice' - def __init__(self, choices): + def __init__(self, choices, case_sensitive=True): self.choices = choices + self.case_sensitive = case_sensitive def get_metavar(self, param): return '[%s]' % '|'.join(self.choices) def get_missing_message(self, param): - return 'Choose from %s.' % ', '.join(self.choices) + return 'Choose from:\n\t%s.' % ',\n\t'.join(self.choices) def convert(self, value, param, ctx): # Exact match if value in self.choices: return value - # Match through normalization + # Match through normalization and case sensitivity + # first do token_normalize_func, then lowercase + # preserve original `value` to produce an accurate message in + # `self.fail` + normed_value = value + normed_choices = self.choices + if ctx is not None and \ ctx.token_normalize_func is not None: - value = ctx.token_normalize_func(value) - for choice in self.choices: - if ctx.token_normalize_func(choice) == value: - return choice + normed_value = ctx.token_normalize_func(value) + normed_choices = [ctx.token_normalize_func(choice) for choice in + self.choices] + + if not self.case_sensitive: + normed_value = normed_value.lower() + normed_choices = [choice.lower() for choice in normed_choices] + + if normed_value in normed_choices: + return normed_value self.fail('invalid choice: %s. (choose from %s)' % (value, ', '.join(self.choices)), param, ctx) @@ -214,23 +234,6 @@ class IntRange(IntParamType): return 'IntRange(%r, %r)' % (self.min, self.max) -class BoolParamType(ParamType): - name = 'boolean' - - def convert(self, value, param, ctx): - if isinstance(value, bool): - return bool(value) - value = value.lower() - if value in ('true', '1', 'yes', 'y'): - return True - elif value in ('false', '0', 'no', 'n'): - return False - self.fail('%s is not a valid boolean' % value, param, ctx) - - def __repr__(self): - return 'BOOL' - - class FloatParamType(ParamType): name = 'float' @@ -245,6 +248,62 @@ class FloatParamType(ParamType): return 'FLOAT' +class FloatRange(FloatParamType): + """A parameter that works similar to :data:`click.FLOAT` but restricts + the value to fit into a range. The default behavior is to fail if the + value falls outside the range, but it can also be silently clamped + between the two edges. + + See :ref:`ranges` for an example. + """ + name = 'float range' + + def __init__(self, min=None, max=None, clamp=False): + self.min = min + self.max = max + self.clamp = clamp + + def convert(self, value, param, ctx): + rv = FloatParamType.convert(self, value, param, ctx) + if self.clamp: + if self.min is not None and rv < self.min: + return self.min + if self.max is not None and rv > self.max: + return self.max + if self.min is not None and rv < self.min or \ + self.max is not None and rv > self.max: + if self.min is None: + self.fail('%s is bigger than the maximum valid value ' + '%s.' % (rv, self.max), param, ctx) + elif self.max is None: + self.fail('%s is smaller than the minimum valid value ' + '%s.' % (rv, self.min), param, ctx) + else: + self.fail('%s is not in the valid range of %s to %s.' + % (rv, self.min, self.max), param, ctx) + return rv + + def __repr__(self): + return 'FloatRange(%r, %r)' % (self.min, self.max) + + +class BoolParamType(ParamType): + name = 'boolean' + + def convert(self, value, param, ctx): + if isinstance(value, bool): + return bool(value) + value = value.lower() + if value in ('true', 't', '1', 'yes', 'y'): + return True + elif value in ('false', 'f', '0', 'no', 'n'): + return False + self.fail('%s is not a valid boolean' % value, param, ctx) + + def __repr__(self): + return 'BOOL' + + class UUIDParameterType(ParamType): name = 'uuid' @@ -273,9 +332,12 @@ class File(ParamType): opened in binary mode or for writing. The encoding parameter can be used to force a specific encoding. - The `lazy` flag controls if the file should be opened immediately or - upon first IO. The default is to be non lazy for standard input and - output streams as well as files opened for reading, lazy otherwise. + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. Starting with Click 2.0, files can also be opened atomically in which case all writes go into a separate file in the same folder and upon @@ -358,14 +420,16 @@ class Path(ParamType): :param readable: if true, a readable check is performed. :param resolve_path: if this is true, then the path is fully resolved before the value is passed onwards. This means - that it's absolute and symlinks are resolved. + that it's absolute and symlinks are resolved. It + will not expand a tilde-prefix, as this is + supposed to be done by the shell only. :param allow_dash: If this is set to `True`, a single dash to indicate standard streams is permitted. - :param type: optionally a string type that should be used to - represent the path. The default is `None` which - means the return value will be either bytes or - unicode depending on what makes most sense given the - input data Click deals with. + :param path_type: optionally a string type that should be used to + represent the path. The default is `None` which + means the return value will be either bytes or + unicode depending on what makes most sense given the + input data Click deals with. """ envvar_list_splitter = os.path.pathsep @@ -384,7 +448,7 @@ class Path(ParamType): if self.file_okay and not self.dir_okay: self.name = 'file' self.path_type = 'File' - if self.dir_okay and not self.file_okay: + elif self.dir_okay and not self.file_okay: self.name = 'directory' self.path_type = 'Directory' else: diff --git a/click/utils.py b/click/utils.py index eee626d..9f175eb 100644 --- a/click/utils.py +++ b/click/utils.py @@ -43,6 +43,7 @@ def make_str(value): def make_default_short_help(help, max_length=45): + """Return a condensed version of help string.""" words = help.split() total_length = 0 result = [] @@ -171,7 +172,7 @@ def echo(message=None, file=None, nl=True, err=False, color=None): Primarily it means that you can print binary data as well as Unicode data on both 2.x and 3.x to the given file in the most appropriate way - possible. This is a very carefree function as in that it will try its + possible. This is a very carefree function in that it will try its best to not fail. As of Click 6.0 this includes support for unicode output on the Windows console. @@ -183,7 +184,7 @@ def echo(message=None, file=None, nl=True, err=False, color=None): - hide ANSI codes automatically if the destination file is not a terminal. - .. _colorama: http://pypi.python.org/pypi/colorama + .. _colorama: https://pypi.org/project/colorama/ .. versionchanged:: 6.0 As of Click 6.0 the echo function will properly support unicode diff --git a/docs/Makefile b/docs/Makefile index 9bca27f..6e3c9b9 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,130 +1,20 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +SPHINXPROJ = Jinja +SOURCEDIR = . BUILDDIR = _build -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -clean: - -rm -rf $(BUILDDIR)/* +.PHONY: help Makefile -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Classy.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Classy.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Classy" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Classy" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -latexpdf: latex - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/click-icon.png b/docs/_static/click-icon.png new file mode 100644 index 0000000..b883cfa Binary files /dev/null and b/docs/_static/click-icon.png differ diff --git a/docs/_static/click-small@2x.png b/docs/_static/click-logo-sidebar.png similarity index 100% rename from docs/_static/click-small@2x.png rename to docs/_static/click-logo-sidebar.png diff --git a/docs/_static/click@2x.png b/docs/_static/click-logo.png similarity index 100% rename from docs/_static/click@2x.png rename to docs/_static/click-logo.png diff --git a/docs/_static/click-small.png b/docs/_static/click-small.png deleted file mode 100644 index f101a8a..0000000 Binary files a/docs/_static/click-small.png and /dev/null differ diff --git a/docs/_static/click.png b/docs/_static/click.png deleted file mode 100644 index 64c62f1..0000000 Binary files a/docs/_static/click.png and /dev/null differ diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html deleted file mode 100644 index 0041190..0000000 --- a/docs/_templates/sidebarintro.html +++ /dev/null @@ -1,13 +0,0 @@ -

About

-

- Click is a Python package for creating beautiful command line interfaces in a - composable way with as little amount of code as necessary. It’s the “Command - Line Interface Creation Kit”. -

-

Useful Links

- diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html deleted file mode 100644 index 55c4629..0000000 --- a/docs/_templates/sidebarlogo.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/docs/advanced.rst b/docs/advanced.rst index f2b83dd..f8c0ed8 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -277,7 +277,7 @@ options: If you go with this solution, the extra arguments will be collected in :attr:`Context.args`. 2. You can attach a :func:`argument` with ``nargs`` set to `-1` which - will eat up all leftover arguments. In this case it's recommeded to + will eat up all leftover arguments. In this case it's recommended to set the `type` to :data:`UNPROCESSED` to avoid any string processing on those arguments as otherwise they are forced into unicode strings automatically which is often not what you want. @@ -295,8 +295,8 @@ In the end you end up with something like this: @click.option('-v', '--verbose', is_flag=True, help='Enables verbose mode') @click.argument('timeit_args', nargs=-1, type=click.UNPROCESSED) def cli(verbose, timeit_args): - """A wrapper around Python's timeit.""" - cmdline = ['python', '-mtimeit'] + list(timeit_args) + """A fake wrapper around Python's timeit.""" + cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args) if verbose: click.echo('Invoking: %s' % ' '.join(cmdline)) call(cmdline) @@ -321,7 +321,7 @@ are important to know about how this ignoring of unhandled flag happens: generally end up like that. Note that because the parser cannot know if an option will accept an argument or not, the ``bar`` part might be handled as an argument. -* Unknown short options might be partially handled and reassmebled if +* Unknown short options might be partially handled and reassembled if necessary. For instance in the above example there is an option called ``-v`` which enables verbose mode. If the command would be ignored with ``-va`` then the ``-v`` part would be handled by Click @@ -346,7 +346,7 @@ Global Context Access .. versionadded:: 5.0 Starting with Click 5.0 it is possible to access the current context from -anywhere within the same through through the use of the +anywhere within the same thread through the use of the :func:`get_current_context` function which returns it. This is primarily useful for accessing the context bound object as well as some flags that are stored on it to customize the runtime behavior. For instance the diff --git a/docs/arguments.rst b/docs/arguments.rst index 1abd88b..b2e61e9 100644 --- a/docs/arguments.rst +++ b/docs/arguments.rst @@ -243,3 +243,21 @@ And from the command line: .. click:run:: invoke(touch, ['--', '-foo.txt', 'bar.txt']) + +If you don't like the ``--`` marker, you can set ignore_unknown_options to +True to avoid checking unknown options: + +.. click:example:: + + @click.command(context_settings={"ignore_unknown_options": True}) + @click.argument('files', nargs=-1, type=click.Path()) + def touch(files): + for filename in files: + click.echo(filename) + +And from the command line: + +.. click:run:: + + invoke(touch, ['-foo.txt', 'bar.txt']) + diff --git a/docs/bashcomplete.rst b/docs/bashcomplete.rst index 9154dfb..bff38fa 100644 --- a/docs/bashcomplete.rst +++ b/docs/bashcomplete.rst @@ -12,17 +12,15 @@ Limitations 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`. Also, Click currently -only supports completion for Bash. - -Currently, Bash completion is an internal feature that is not customizable. -This might be relaxed in future versions. +how to do that, see :ref:`setuptools-integration`. Click currently +only supports completion for Bash and Zsh. What it Completes ----------------- -Generally, the Bash completion support will complete subcommands and -parameters. Subcommands are always listed whereas parameters only if at +Generally, the Bash completion support will complete subcommands, options +and any option or argument values where the type is click.Choice. +Subcommands and choices are always listed whereas options only if at least a dash has been provided. Example:: $ repo @@ -30,6 +28,60 @@ least a dash has been provided. Example:: $ repo clone - --deep --help --rev --shallow -r +Additionally, custom suggestions can be provided for arguments and options with +the ``autocompletion`` parameter. ``autocompletion`` should a callback function +that returns a list of strings. This is useful when the suggestions need to be +dynamically generated at bash completion time. The callback function will be +passed 3 keyword arguments: + +- ``ctx`` - The current click context. +- ``args`` - The list of arguments passed in. +- ``incomplete`` - The partial word that is being completed, as a string. May + be an empty string ``''`` if no characters have been entered yet. + +Here is an example of using a callback function to generate dynamic suggestions: + +.. click:example:: + + import os + + def get_env_vars(ctx, args, incomplete): + return [k for k in os.environ.keys() if incomplete in k] + + @click.command() + @click.argument("envvar", type=click.STRING, autocompletion=get_env_vars) + def cmd1(envvar): + click.echo('Environment variable: %s' % envvar) + click.echo('Value: %s' % os.environ[envvar]) + + +Completion help strings (ZSH only) +---------------------------------- + +ZSH supports showing documentation strings for completions. These are taken +from the help parameters of options and subcommands. For dynamically generated +completions a help string can be provided by returning a tuple instead of a +string. The first element of the tuple is the completion and the second is the +help string to display. + +Here is an example of using a callback function to generate dynamic suggestions with help strings: + +.. click:example:: + + import os + + def get_colors(ctx, args, incomplete): + colors = [('red', 'help string for the color red'), + ('blue', 'help string for the color blue'), + ('green', 'help string for the color green')] + return [c for c in colors if incomplete in c[0]] + + @click.command() + @click.argument("color", type=click.STRING, autocompletion=get_colors) + def cmd1(color): + click.echo('Chosen color is %s' % color) + + Activation ---------- @@ -42,14 +94,18 @@ with dashes replaced by underscores. If your tool is called ``foo-bar``, then the magic variable is called ``_FOO_BAR_COMPLETE``. By exporting it with the ``source`` value it will -spit out the activation script which can be trivally activated. +spit out the activation script which can be trivially activated. For instance, to enable Bash completion for your ``foo-bar`` script, this is what you would need to put into your ``.bashrc``:: eval "$(_FOO_BAR_COMPLETE=source foo-bar)" -From this point onwards, your script will have Bash completion enabled. +For zsh users add this to your ``.zshrc``:: + + eval "$(_FOO_BAR_COMPLETE=source_zsh foo-bar)" + +From this point onwards, your script will have autocompletion enabled. Activation Script ----------------- @@ -64,6 +120,12 @@ This can be easily accomplished:: _FOO_BAR_COMPLETE=source foo-bar > foo-bar-complete.sh -And then you would put this into your bashrc instead:: +For zsh: + + _FOO_BAR_COMPLETE=source_zsh foo-bar > foo-bar-complete.sh + +And then you would put this into your .bashrc or .zshrc instead:: . /path/to/foo-bar-complete.sh + + diff --git a/docs/changelog.rst b/docs/changelog.rst index 9db9592..7e0e1bb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,3 +1,3 @@ .. currentmodule:: click -.. include:: ../CHANGES +.. include:: ../CHANGES.rst diff --git a/docs/clickdoctools.py b/docs/clickdoctools.py deleted file mode 100644 index 36723fa..0000000 --- a/docs/clickdoctools.py +++ /dev/null @@ -1,275 +0,0 @@ -import os -import sys -import click -import shutil -import tempfile -import contextlib -import subprocess - -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -from docutils import nodes -from docutils.statemachine import ViewList - -from sphinx.domains import Domain -from sphinx.util.compat import Directive - - -class EchoingStdin(object): - - def __init__(self, input, output): - self._input = input - self._output = output - - def __getattr__(self, x): - return getattr(self._input, x) - - def _echo(self, rv): - mark = False - if rv.endswith('\xff'): - rv = rv[:-1] - mark = True - self._output.write(rv) - if mark: - self._output.write('^D\n') - return rv - - def read(self, n=-1): - return self._echo(self._input.read(n)) - - def readline(self, n=-1): - return self._echo(self._input.readline(n)) - - def readlines(self): - return [self._echo(x) for x in self._input.readlines()] - - def __iter__(self): - return iter(self._echo(x) for x in self._input) - - -@contextlib.contextmanager -def fake_modules(): - old_call = subprocess.call - def dummy_call(*args, **kwargs): - with tempfile.TemporaryFile('wb+') as f: - kwargs['stdout'] = f - kwargs['stderr'] = f - rv = subprocess.Popen(*args, **kwargs).wait() - f.seek(0) - click.echo(f.read().decode('utf-8', 'replace').rstrip()) - return rv - subprocess.call = dummy_call - try: - yield - finally: - subprocess.call = old_call - - -@contextlib.contextmanager -def isolation(input=None, env=None): - if isinstance(input, unicode): - input = input.encode('utf-8') - input = StringIO(input or '') - output = StringIO() - sys.stdin = EchoingStdin(input, output) - sys.stdin.encoding = 'utf-8' - - def visible_input(prompt=None): - sys.stdout.write(prompt or '') - val = input.readline().rstrip('\r\n') - sys.stdout.write(val + '\n') - sys.stdout.flush() - return val - - def hidden_input(prompt=None): - sys.stdout.write((prompt or '') + '\n') - sys.stdout.flush() - return input.readline().rstrip('\r\n') - - sys.stdout = output - sys.stderr = output - old_visible_prompt_func = click.termui.visible_prompt_func - old_hidden_prompt_func = click.termui.hidden_prompt_func - click.termui.visible_prompt_func = visible_input - click.termui.hidden_prompt_func = hidden_input - - old_env = {} - try: - if env: - for key, value in env.iteritems(): - old_env[key] = os.environ.get(value) - os.environ[key] = value - yield output - finally: - for key, value in old_env.iteritems(): - if value is None: - try: - del os.environ[key] - except Exception: - pass - else: - os.environ[key] = value - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - click.termui.visible_prompt_func = old_visible_prompt_func - click.termui.hidden_prompt_func = old_hidden_prompt_func - - -@contextlib.contextmanager -def isolated_filesystem(): - cwd = os.getcwd() - t = tempfile.mkdtemp() - os.chdir(t) - try: - yield - finally: - os.chdir(cwd) - try: - shutil.rmtree(t) - except (OSError, IOError): - pass - - -class ExampleRunner(object): - - def __init__(self): - self.namespace = { - 'click': click, - '__file__': 'dummy.py', - } - - def declare(self, source): - with fake_modules(): - code = compile(source, '', 'exec') - eval(code, self.namespace) - - def run(self, source): - code = compile(source, '', 'exec') - buffer = [] - - def invoke(cmd, args=None, prog_name=None, - input=None, terminate_input=False, env=None, - **extra): - if env: - for key, value in sorted(env.items()): - if ' ' in value: - value = '"%s"' % value - buffer.append('$ export %s=%s' % (key, value)) - args = args or [] - if prog_name is None: - prog_name = cmd.name.replace('_', '-') - buffer.append(('$ %s %s' % ( - prog_name, - ' '.join(('"%s"' % x) if ' ' in x else x for x in args) - )).rstrip()) - if isinstance(input, (tuple, list)): - input = '\n'.join(input) + '\n' - if terminate_input: - input += '\xff' - with isolation(input=input, env=env) as output: - try: - cmd.main(args=args, prog_name=prog_name.split()[-1], - **extra) - except SystemExit: - pass - buffer.extend(output.getvalue().splitlines()) - - def println(text=''): - buffer.append(text) - - eval(code, self.namespace, { - 'invoke': invoke, - 'println': println, - 'isolated_filesystem': isolated_filesystem, - }) - return buffer - - def close(self): - pass - - -def parse_rst(state, content_offset, doc): - node = nodes.section() - # hack around title style bookkeeping - surrounding_title_styles = state.memo.title_styles - surrounding_section_level = state.memo.section_level - state.memo.title_styles = [] - state.memo.section_level = 0 - state.nested_parse(doc, content_offset, node, match_titles=1) - state.memo.title_styles = surrounding_title_styles - state.memo.section_level = surrounding_section_level - return node.children - - -def get_example_runner(document): - runner = getattr(document, 'click_example_runner', None) - if runner is None: - runner = document.click_example_runner = ExampleRunner() - return runner - - -class ExampleDirective(Directive): - has_content = True - required_arguments = 0 - optional_arguments = 0 - final_argument_whitespace = False - - def run(self): - doc = ViewList() - runner = get_example_runner(self.state.document) - try: - runner.declare('\n'.join(self.content)) - except: - runner.close() - raise - doc.append('.. sourcecode:: python', '') - doc.append('', '') - for line in self.content: - doc.append(' ' + line, '') - return parse_rst(self.state, self.content_offset, doc) - - -class RunExampleDirective(Directive): - has_content = True - required_arguments = 0 - optional_arguments = 0 - final_argument_whitespace = False - - def run(self): - doc = ViewList() - runner = get_example_runner(self.state.document) - try: - rv = runner.run('\n'.join(self.content)) - except: - runner.close() - raise - doc.append('.. sourcecode:: text', '') - doc.append('', '') - for line in rv: - doc.append(' ' + line, '') - return parse_rst(self.state, self.content_offset, doc) - - -class ClickDomain(Domain): - name = 'click' - label = 'Click' - directives = { - 'example': ExampleDirective, - 'run': RunExampleDirective, - } - - -def delete_example_runner_state(app, doctree): - runner = getattr(doctree, 'click_example_runner', None) - if runner is not None: - runner.close() - del doctree.click_example_runner - - -def setup(app): - app.add_domain(ClickDomain) - - app.connect('doctree-read', delete_example_runner_state) diff --git a/docs/commands.rst b/docs/commands.rst index 95d4fa8..53834f7 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -29,7 +29,7 @@ when an inner command runs: @cli.command() def sync(): - click.echo('Synching') + click.echo('Syncing') Here is what this looks like: @@ -87,6 +87,10 @@ script like this: @click.option('--debug/--no-debug', default=False) @click.pass_context def cli(ctx, debug): + # ensure that ctx.obj exists and is a dict (in case `cli()` is called + # by means other than the `if` block below + ctx.ensure_object(dict) + ctx.obj['DEBUG'] = debug @cli.command() @@ -417,7 +421,7 @@ to not use the file type and manually open the file through For a more complex example that also improves upon handling of the pipelines have a look at the `imagepipe multi command chaining demo -`__ in +`__ in the Click repository. It implements a pipeline based image editing tool that has a nice internal structure for the pipelines. @@ -437,7 +441,7 @@ you're not satisfied with the defaults. The default map can be nested arbitrarily for each subcommand and provided when the script is invoked. Alternatively, it can also be -overriden at any point by commands. For instance, a top-level command could +overridden at any point by commands. For instance, a top-level command could load the defaults from a configuration file. Example usage: diff --git a/docs/complex.rst b/docs/complex.rst index 794de2d..e9cefb5 100644 --- a/docs/complex.rst +++ b/docs/complex.rst @@ -153,10 +153,10 @@ One obvious way to remedy this is to store a reference to the repo in the plugin, but then a command needs to be aware that it's attached below such a plugin. -There is a much better system that can built by taking advantage of the linked -nature of contexts. We know that the plugin context is linked to the context -that created our repo. Because of that, we can start a search for the last -level where the object stored by the context was a repo. +There is a much better system that can be built by taking advantage of the +linked nature of contexts. We know that the plugin context is linked to the +context that created our repo. Because of that, we can start a search for +the last level where the object stored by the context was a repo. Built-in support for this is provided by the :func:`make_pass_decorator` factory, which will create decorators for us that find objects (it @@ -187,8 +187,9 @@ The above example only works if there was an outer command that created a ``Repo`` object and stored it in the context. For some more advanced use cases, this might become a problem. The default behavior of :func:`make_pass_decorator` is to call :meth:`Context.find_object` -which will find the object. If it can't find the object, it will raise an -error. The alternative behavior is to use :meth:`Context.ensure_object` +which will find the object. If it can't find the object, +:meth:`make_pass_decorator` will raise an error. +The alternative behavior is to use :meth:`Context.ensure_object` which will find the object, and if it cannot find it, will create one and store it in the innermost context. This behavior can also be enabled for :func:`make_pass_decorator` by passing ``ensure=True``: @@ -210,7 +211,7 @@ As such it runs standalone: @click.command() @pass_repo def cp(repo): - click.echo(repo) + click.echo(isinstance(repo, Repo)) As you can see: diff --git a/docs/conf.py b/docs/conf.py index dd69995..d64f8fa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,219 +1,47 @@ -# -*- coding: utf-8 -*- -# -# click documentation build configuration file, created by -# sphinx-quickstart on Mon Apr 26 19:53:01 2010. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +from pallets_sphinx_themes import ProjectLink, get_version -import sys, os -import datetime +# Project -------------------------------------------------------------- -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.abspath('..')) -sys.path.append(os.path.abspath('.')) +project = "Click" +copyright = "2014 Pallets Team" +author = "Pallets Team" +release, version = get_version("Click") -# -- General configuration ----------------------------------------------------- +# General -------------------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +master_doc = "index" +extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "pallets_sphinx_themes"] +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', - 'clickdoctools'] +# HTML ----------------------------------------------------------------- -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'click' -copyright = u'%d, Armin Ronacher' % datetime.datetime.utcnow().year - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '1.0' -# The full version, including alpha/beta/rc tags. -release = '1.0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -#html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. +html_theme = "click" html_theme_options = { + "index_sidebar_logo": False, +} +html_context = { + "project_links": [ + ProjectLink("Donate to Pallets", "https://palletsprojects.com/donate"), + ProjectLink("Click Website", "https://palletsprojects.com/p/click/"), + ProjectLink("PyPI releases", "https://pypi.org/project/Click/"), + ProjectLink("Source Code", "https://github.com/pallets/click/"), + ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"), + ] } - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = ['_themes'] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -html_title = 'click' - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. html_sidebars = { - 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], - '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', - 'sourcelink.html', 'searchbox.html'] + "index": ["project.html", "versions.html", "searchbox.html"], + "**": ["localtoc.html", "relations.html", "versions.html", "searchbox.html"], } +singlehtml_sidebars = {"index": ["project.html", "versions.html", "localtoc.html"]} +html_static_path = ["_static"] +html_favicon = "_static/click-icon.png" +html_logo = "_static/click-logo-sidebar.png" +html_show_sourcelink = False +html_domain_indices = False +html_experimental_html5_writer = True -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} +# LaTeX ---------------------------------------------------------------- -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'clickdoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'click.tex', u'click documentation', - u'Armin Ronacher', 'manual'), + (master_doc, "Click.tex", "Click Documentation", "Pallets Team", "manual") ] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'click', u'click documentation', - [u'Armin Ronacher'], 1) -] - -intersphinx_mapping = { - 'http://docs.python.org/dev': None -} diff --git a/docs/documentation.rst b/docs/documentation.rst index 13b92f0..6d582e0 100644 --- a/docs/documentation.rst +++ b/docs/documentation.rst @@ -72,6 +72,41 @@ Example: And what it looks like: +.. click:run:: + + invoke(cli, args=['--help']) + +.. _doc-meta-variables: + +Truncating Help Texts +--------------------- + +Click gets command help text from function docstrings. However if you +already use docstrings to document function arguments you may not want +to see :param: and :return: lines in your help text. + +You can use the ``\f`` escape marker to have Click truncate the help text +after the marker. + +Example: + +.. click:example:: + + @click.command() + @click.pass_context + def cli(ctx): + """First paragraph. + + This is a very long second + paragraph and not correctly + wrapped but it will be rewrapped. + \f + + :param click.core.Context ctx: Click context. + """ + +And what it looks like: + .. click:run:: invoke(cli, args=['--help']) diff --git a/docs/index.rst b/docs/index.rst index 8cb7778..7fac849 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,12 @@ -Welcome to the Click Documentation -================================== +.. rst-class:: hide-header + +Welcome to Click +================ + +.. image:: _static/click-logo.png + :align: center + :scale: 50% + :target: https://palletsprojects.com/p/click/ Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It's the "Command @@ -50,8 +57,8 @@ You can get the library directly from PyPI:: pip install click -Documentation Contents ----------------------- +Documentation +------------- This part of the documentation guides you through all of the library's usage patterns. diff --git a/docs/make.bat b/docs/make.bat index 1e941d8..1479463 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,155 +1,36 @@ @ECHO OFF +pushd %~dp0 + REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) +set SOURCEDIR=. set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% -) +set SPHINXPROJ=Jinja if "%1" == "" goto help -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 ) -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Classy.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Classy.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end +popd diff --git a/docs/options.rst b/docs/options.rst index 2a01802..b1d0dcf 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -10,14 +10,38 @@ decorator. Since options can come in various different versions, there are a ton of parameters to configure their behavior. Options in click are distinct from :ref:`positional arguments `. +Name Your Options +----------------- + +The naming rules can be found in :ref:`parameter_names`. In short, you +can refer the option **implicitly** by the longest dash-prefixed argument: + +.. click:example:: + + @click.command() + @click.option('-s', '--string-to-echo') + def echo(string_to_echo): + click.echo(string_to_echo) + +Or, **explicitly**, by giving one non-dash-prefixed argument: + +.. click:example:: + + @click.command() + @click.option('-s', '--string-to-echo', 'string') + def echo(string): + click.echo(string) + Basic Value Options ------------------- The most basic option is a value option. These options accept one argument which is a value. If no type is provided, the type of the default value is used. If no default value is provided, the type is assumed to be -:data:`STRING`. By default, the name of the parameter is the first long -option defined; otherwise the first short one is used. +:data:`STRING`. Unless a name is explicitly specified, the name of the +parameter is the first long option defined; otherwise the first short one is +used. By default, options are not required, however to make an option required, +simply pass in `required=True` as an argument to the decorator. .. click:example:: @@ -26,6 +50,23 @@ option defined; otherwise the first short one is used. def dots(n): click.echo('.' * n) +.. click:example:: + + # How to make an option required + @click.command() + @click.option('--n', required=True, type=int) + def dots(n): + click.echo('.' * n) + +.. click:example:: + + # How to use a Python reserved word such as `from` as a parameter + @click.command() + @click.option('--from', '-f', 'from_') + @click.option('--to', '-t') + def reserved_param_name(from_, to): + click.echo('from %s to %s' % (from_, to)) + And on the command line: .. click:run:: @@ -35,6 +76,19 @@ And on the command line: In this case the option is of type :data:`INT` because the default value is an integer. +To show the default values when showing command help, use ``show_default=True`` + +.. click:example:: + + @click.command() + @click.option('--n', default=1, show_default=True) + def dots(n): + click.echo('.' * n) + +.. click:run:: + + invoke(dots, args=['--help']) + Multi Value Options ------------------- @@ -70,7 +124,7 @@ the tuple. For this you can directly specify a tuple as type: .. click:example:: @click.command() - @click.option('--item', type=(unicode, int)) + @click.option('--item', type=(str, int)) def putitem(item): click.echo('name=%s id=%d' % item) @@ -87,7 +141,7 @@ used. The above example is thus equivalent to this: .. click:example:: @click.command() - @click.option('--item', nargs=2, type=click.Tuple([unicode, int])) + @click.option('--item', nargs=2, type=click.Tuple([str, int])) def putitem(item): click.echo('name=%s id=%d' % item) @@ -282,6 +336,11 @@ What it looks like: println() invoke(digest, args=['--help']) +.. note:: + + You should only pass the choices as list or tuple. Other iterables (like + generators) may lead to surprising results. + .. _option-prompting: Prompting @@ -374,6 +433,21 @@ from the environment: def hello(username): print("Hello,", username) +To describe what the default value will be, set it in ``show_default``. + +.. click:example:: + + @click.command() + @click.option('--username', prompt=True, + default=lambda: os.environ.get('USER', ''), + show_default='current user') + def hello(username): + print("Hello,", username) + +.. click:run:: + + invoke(hello, args=['--help']) + Callbacks and Eager Options --------------------------- @@ -514,6 +588,33 @@ And from the command line: invoke(greet, env={'GREETER_USERNAME': 'john'}, auto_envvar_prefix='GREETER') +When using ``auto_envvar_prefix`` with command groups, the command name needs +to be included in the environment variable, between the prefix and the parameter name, *i.e.* *PREFIX_COMMAND_VARIABLE*. + +Example: + +.. click:example:: + + @click.group() + @click.option('--debug/--no-debug') + def cli(debug): + click.echo('Debug mode is %s' % ('on' if debug else 'off')) + + @cli.command() + @click.option('--username') + def greet(username): + click.echo('Hello %s!' % username) + + if __name__ == '__main__': + cli(auto_envvar_prefix='GREETER') + +.. click:run:: + + invoke(cli, args=['greet',], + env={'GREETER_GREET_USERNAME': 'John', 'GREETER_DEBUG': 'false'}, + auto_envvar_prefix='GREETER') + + The second option is to manually pull values in from specific environment variables by defining the name of the environment variable on the option. diff --git a/docs/parameters.rst b/docs/parameters.rst index 920b9be..112e8fa 100644 --- a/docs/parameters.rst +++ b/docs/parameters.rst @@ -47,8 +47,8 @@ different behavior and some are supported out of the box: ``bool`` / :data:`click.BOOL`: A parameter that accepts boolean values. This is automatically used - for boolean flags. If used with string values ``1``, ``yes``, ``y`` - and ``true`` convert to `True` and ``0``, ``no``, ``n`` and ``false`` + for boolean flags. If used with string values ``1``, ``yes``, ``y``, ``t`` + and ``true`` convert to `True` and ``0``, ``no``, ``n``, ``f`` and ``false`` convert to `False`. :data:`click.UUID`: @@ -67,25 +67,41 @@ different behavior and some are supported out of the box: .. autoclass:: IntRange :noindex: +.. autoclass:: FloatRange + :noindex: + Custom parameter types can be implemented by subclassing :class:`click.ParamType`. For simple cases, passing a Python function that fails with a `ValueError` is also supported, though discouraged. +.. _parameter_names: + Parameter Names --------------- -Parameters (both options and arguments) accept a number of positional -arguments which are the parameter declarations. Each string with a -single dash is added as short argument; each string starting with a double -dash as long one. If a string is added without any dashes, it becomes the -internal parameter name which is also used as variable name. +Parameters (both options and arguments) accept a number of positional arguments +which are passed to the command function as parameters. Each string with a +single dash is added as a short argument; each string starting with a double +dash as a long one. -If a parameter is not given a name without dashes, a name is generated +If a string is added without any dashes, it becomes the internal parameter name +which is also used as variable name. + +If all names for a parameter contain dashes, the internal name is generated automatically by taking the longest argument and converting all dashes to -underscores. 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 is -called `dest`. +underscores. + +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 ------------------------- diff --git a/docs/python3.rst b/docs/python3.rst index e148ba6..65e1b50 100644 --- a/docs/python3.rst +++ b/docs/python3.rst @@ -6,9 +6,9 @@ Python 3 Support Click supports Python 3, but like all other command line utility libraries, it suffers from the Unicode text model in Python 3. All examples in the documentation were written so that they could run on both Python 2.x and -Python 3.3 or higher. +Python 3.4 or higher. -At the moment, it is strongly recommended is to use Python 2 for Click +At the moment, it is strongly recommended to use Python 2 for Click utilities unless Python 3 is a hard requirement. .. _python3-limitations: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e5ce571..4824f66 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -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 the `sudo` prefix. -.. _installing pip: http://pip.readthedocs.org/en/latest/installing.html +.. _installing pip: https://pip.readthedocs.io/en/latest/installing.html Once you have virtualenv installed, just fire up a shell and create your own environment. I usually create a project folder and a `venv` @@ -79,7 +79,7 @@ And if you want to go back to the real world, use the following command:: $ deactivate -After doing this, the prompt of your shell should be as familar as before. +After doing this, the prompt of your shell should be as familiar as before. Now, let's move on. Enter the following command to get Click activated in your virtualenv:: @@ -102,26 +102,26 @@ Examples of Click applications can be found in the documentation as well as in the GitHub repository together with readme files: * ``inout``: `File input and output - `_ + `_ * ``naval``: `Port of docopt naval example - `_ + `_ * ``aliases``: `Command alias example - `_ + `_ * ``repo``: `Git-/Mercurial-like command line interface - `_ + `_ * ``complex``: `Complex example with plugin loading - `_ + `_ * ``validation``: `Custom parameter validation example - `_ + `_ * ``colors``: `Colorama ANSI color support - `_ + `_ * ``termui``: `Terminal UI functions demo - `_ + `_ * ``imagepipe``: `Multi command chaining demo - `_ + `_ -Basic Concepts --------------- +Basic Concepts - Creating a Command +----------------------------------- Click is based on declaring commands through decorators. Internally, there is a non-decorator interface for advanced use cases, but it's discouraged @@ -174,7 +174,9 @@ correction in case the terminal is misconfigured instead of dying with an As an added benefit, starting with Click 2.0, the echo function also has good support for ANSI colors. It will automatically strip ANSI codes if the output stream is a file and if colorama is supported, ANSI colors -will also work on Windows. See :ref:`ansi-colors` for more information. +will also work on Windows. Note that in Python 2, the :func:`echo` function +does not parse color code information from bytearrays. See :ref:`ansi-colors` +for more information. If you don't need this, you can also use the `print()` construct / function. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..6bb1491 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +Sphinx +Pallets-Sphinx-Themes diff --git a/docs/testing.rst b/docs/testing.rst index 3c046bf..174b775 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -61,6 +61,11 @@ Example:: 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() + result = runner.invoke(cli, ['--debug', 'sync'], terminal_width=60) + File System Isolation --------------------- diff --git a/docs/upgrading.rst b/docs/upgrading.rst index b682d21..7c4cccd 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -44,7 +44,7 @@ properly by this function. Multicommand Chaining API ````````````````````````` -Click 3 introduced multicommand chaning. This required a change in how +Click 3 introduced multicommand chaining. This required a change in how Click internally dispatches. Unfortunately this change was not correctly implemented and it appeared that it was possible to provide an API that can inform the super command about all the subcommands that will be diff --git a/docs/utils.rst b/docs/utils.rst index f1798b9..f16a57d 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -41,7 +41,7 @@ new in Click 6.0. Click now emulates output streams on Windows to support unicode to the Windows console through separate APIs. For more information see -`wincmd`_. +:doc:`wincmd`. .. versionadded:: 3.0 @@ -61,7 +61,8 @@ ANSI Colors Starting with Click 2.0, the :func:`echo` function gained extra functionality to deal with ANSI colors and styles. Note that on Windows, this functionality is only available if `colorama`_ is installed. If it -is installed, then ANSI codes are intelligently handled. +is installed, then ANSI codes are intelligently handled. Note that in Python +2, the echo function doesn't parse color code information from bytearrays. Primarily this means that: @@ -95,7 +96,7 @@ a single function called :func:`secho`:: click.secho('ATTENTION', blink=True, bold=True) -.. _colorama: https://pypi.python.org/pypi/colorama +.. _colorama: https://pypi.org/project/colorama/ Pager Support ------------- @@ -114,6 +115,17 @@ Example: click.echo_via_pager('\n'.join('Line %d' % idx for idx in range(200))) +If you want to use the pager for a lot of text, especially if generating everything in advance would take a lot of time, you can pass a generator (or generator function) instead of a string: + +.. click:example:: + def _generate_output(): + for idx in range(50000): + yield "Line %d\n" % idx + + @click.command() + def less(): + click.echo_via_pager(_generate_output()) + Screen Clearing --------------- @@ -226,7 +238,7 @@ Launching Applications .. versionadded:: 2.0 Click supports launching applications through :func:`launch`. This can be -used to open the default application assocated with a URL or filetype. +used to open the default application associated with a URL or filetype. This can be used to launch web browsers or picture viewers, for instance. In addition to this, it can also launch the file manager and automatically select the provided file. @@ -266,7 +278,7 @@ streams respond to Unicode and binary data. Because of this, click provides the :func:`get_binary_stream` and :func:`get_text_stream` functions, which produce consistent results with -different Python versions and for a wide variety pf terminal configurations. +different Python versions and for a wide variety of terminal configurations. The end result is that these functions will always return a functional stream object (except in very odd cases in Python 3; see @@ -283,7 +295,7 @@ Example:: Click now emulates output streams on Windows to support unicode to the Windows console through separate APIs. For more information see -`wincmd`_. +:doc:`wincmd`. Intelligent File Opening diff --git a/docs/why.rst b/docs/why.rst index cd9c037..76b84e7 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -8,7 +8,7 @@ This question is easy to answer: because there is not a single command line utility for Python out there which ticks the following boxes: * is lazily composable without restrictions -* fully follows the Unix command line conventions +* supports implementation of Unix/POSIX command line conventions * supports loading values from environment variables out of the box * supports for prompting of custom values * is fully nestable and composable diff --git a/examples/bashcompletion/README b/examples/bashcompletion/README new file mode 100644 index 0000000..f8a0d51 --- /dev/null +++ b/examples/bashcompletion/README @@ -0,0 +1,12 @@ +$ bashcompletion + + bashcompletion is a simple example of an application that + tries to autocomplete commands, arguments and options. + + This example requires Click 2.0 or higher. + +Usage: + + $ pip install --editable . + $ eval "$(_BASHCOMPLETION_COMPLETE=source bashcompletion)" + $ bashcompletion --help diff --git a/examples/bashcompletion/bashcompletion.py b/examples/bashcompletion/bashcompletion.py new file mode 100644 index 0000000..4d9fdde --- /dev/null +++ b/examples/bashcompletion/bashcompletion.py @@ -0,0 +1,41 @@ +import click +import os + + +@click.group() +def cli(): + pass + + +def get_env_vars(ctx, args, incomplete): + for key in os.environ.keys(): + if incomplete in key: + yield key + + +@cli.command(help='A command to print environment variables') +@click.argument("envvar", type=click.STRING, autocompletion=get_env_vars) +def cmd1(envvar): + click.echo('Environment variable: %s' % envvar) + click.echo('Value: %s' % os.environ[envvar]) + + +@click.group(help='A group that holds a subcommand') +def group(): + pass + + +def list_users(ctx, args, incomplete): + # Here you can generate completions dynamically + users = ['bob', 'alice'] + for user in users: + if user.startswith(incomplete): + yield user + + +@group.command(help='Choose a user') +@click.argument("user", type=click.STRING, autocompletion=list_users) +def subcmd(user): + click.echo('Chosen user is %s' % user) + +cli.add_command(group) diff --git a/examples/bashcompletion/setup.py b/examples/bashcompletion/setup.py new file mode 100644 index 0000000..ad20081 --- /dev/null +++ b/examples/bashcompletion/setup.py @@ -0,0 +1,15 @@ +from setuptools import setup + +setup( + name='click-example-bashcompletion', + version='1.0', + py_modules=['bashcompletion'], + include_package_data=True, + install_requires=[ + 'click', + ], + entry_points=''' + [console_scripts] + bashcompletion=bashcompletion:cli + ''', +) diff --git a/examples/colors/colors.py b/examples/colors/colors.py index 1e365bd..193b927 100644 --- a/examples/colors/colors.py +++ b/examples/colors/colors.py @@ -2,7 +2,9 @@ import click all_colors = 'black', 'red', 'green', 'yellow', 'blue', 'magenta', \ - 'cyan', 'white' + 'cyan', 'white', 'bright_black', 'bright_red', \ + 'bright_green', 'bright_yellow', 'bright_blue', \ + 'bright_magenta', 'bright_cyan', 'bright_white' @click.command() diff --git a/examples/validation/validation.py b/examples/validation/validation.py index 4b95091..00fa0a6 100644 --- a/examples/validation/validation.py +++ b/examples/validation/validation.py @@ -1,6 +1,6 @@ import click try: - from urllib import parser as urlparse + from urllib import parse as urlparse except ImportError: import urlparse diff --git a/setup.cfg b/setup.cfg index 6c71b61..3b0846a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,8 @@ -[wheel] -universal = 1 +[bdist_wheel] +universal=1 -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 +[metadata] +license_file = LICENSE +[tool:pytest] +addopts = -p no:warnings --tb=short diff --git a/setup.py b/setup.py index 8bacd6a..194d1d4 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import io import re -import ast from setuptools import setup _version_re = re.compile(r'__version__\s+=\s+(.*)') -with open('click/__init__.py', 'rb') as f: - version = str(ast.literal_eval(_version_re.search( - f.read().decode('utf-8')).group(1))) +with io.open('README.rst', 'rt', encoding='utf8') as f: + readme = f.read() +with io.open('click/__init__.py', 'rt', encoding='utf8') as f: + version = re.search(r'__version__ = \'(.*?)\'', f.read()).group(1) setup( name='click', + version=version, + url='https://palletsprojects.com/p/click/', author='Armin Ronacher', author_email='armin.ronacher@active-4.com', - version=version, - url='http://github.com/mitsuhiko/click', + maintainer='Pallets team', + maintainer_email='contact@palletsprojects.com', + long_description=readme, packages=['click'], description='A simple wrapper around optparse for ' 'powerful command line utilities.', + license='BSD', + extras_require={ + 'dev': [ + 'pytest>=3', + 'coverage', + 'tox', + 'sphinx', + ], + 'docs': [ + 'sphinx', + 'Pallets-Sphinx-Themes', + ] + }, classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ], + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", ) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 1972976..a6a3258 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -188,7 +188,7 @@ def test_empty_nargs(runner): result = runner.invoke(cmd2, []) assert result.exit_code == 2 - assert 'Missing argument "arg"' in result.output + assert 'Missing argument "ARG..."' in result.output def test_missing_arg(runner): @@ -199,7 +199,7 @@ def test_missing_arg(runner): result = runner.invoke(cmd, []) assert result.exit_code == 2 - assert 'Missing argument "arg".' in result.output + assert 'Missing argument "ARG".' in result.output def test_implicit_non_required(runner): diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py index fea096c..0390841 100644 --- a/tests/test_bashcomplete.py +++ b/tests/test_bashcomplete.py @@ -4,14 +4,59 @@ import click from click._bashcomplete import get_choices +def choices_without_help(cli, args, incomplete): + completions = get_choices(cli, 'dummy', args, incomplete) + return [c[0] for c in completions] + + +def choices_with_help(cli, args, incomplete): + return list(get_choices(cli, 'dummy', args, incomplete)) + + def test_single_command(): @click.command() @click.option('--local-opt') def cli(local_opt): pass - assert list(get_choices(cli, 'lol', [], '-')) == ['--local-opt'] - assert list(get_choices(cli, 'lol', [], '')) == [] + assert choices_without_help(cli, [], '-') == ['--local-opt'] + assert choices_without_help(cli, [], '') == [] + + +def test_boolean_flag(): + @click.command() + @click.option('--shout/--no-shout', default=False) + def cli(local_opt): + pass + + assert choices_without_help(cli, [], '-') == ['--shout', '--no-shout'] + + +def test_multi_value_option(): + @click.group() + @click.option('--pos', nargs=2, type=float) + def cli(local_opt): + pass + + @cli.command() + @click.option('--local-opt') + def sub(local_opt): + pass + + assert choices_without_help(cli, [], '-') == ['--pos'] + assert choices_without_help(cli, ['--pos'], '') == [] + assert choices_without_help(cli, ['--pos', '1.0'], '') == [] + assert choices_without_help(cli, ['--pos', '1.0', '1.0'], '') == ['sub'] + + +def test_multi_option(): + @click.command() + @click.option('--message', '-m', multiple=True) + def cli(local_opt): + pass + + assert choices_without_help(cli, [], '-') == ['--message', '-m'] + assert choices_without_help(cli, ['-m'], '') == [] def test_small_chain(): @@ -25,10 +70,10 @@ def test_small_chain(): def sub(local_opt): pass - assert list(get_choices(cli, 'lol', [], '')) == ['sub'] - assert list(get_choices(cli, 'lol', [], '-')) == ['--global-opt'] - assert list(get_choices(cli, 'lol', ['sub'], '')) == [] - assert list(get_choices(cli, 'lol', ['sub'], '-')) == ['--local-opt'] + assert choices_without_help(cli, [], '') == ['sub'] + assert choices_without_help(cli, [], '-') == ['--global-opt'] + assert choices_without_help(cli, ['sub'], '') == [] + assert choices_without_help(cli, ['sub'], '-') == ['--local-opt'] def test_long_chain(): @@ -47,16 +92,206 @@ def test_long_chain(): def bsub(bsub_opt): pass + COLORS = ['red', 'green', 'blue'] + def get_colors(ctx, args, incomplete): + for c in COLORS: + if c.startswith(incomplete): + yield c + + def search_colors(ctx, args, incomplete): + for c in COLORS: + if incomplete in c: + yield c + + CSUB_OPT_CHOICES = ['foo', 'bar'] + CSUB_CHOICES = ['bar', 'baz'] @bsub.command('csub') - @click.option('--csub-opt') - def csub(csub_opt): + @click.option('--csub-opt', type=click.Choice(CSUB_OPT_CHOICES)) + @click.option('--csub', type=click.Choice(CSUB_CHOICES)) + @click.option('--search-color', autocompletion=search_colors) + @click.argument('color', autocompletion=get_colors) + def csub(csub_opt, color): pass - assert list(get_choices(cli, 'lol', [], '-')) == ['--cli-opt'] - assert list(get_choices(cli, 'lol', [], '')) == ['asub'] - assert list(get_choices(cli, 'lol', ['asub'], '-')) == ['--asub-opt'] - assert list(get_choices(cli, 'lol', ['asub'], '')) == ['bsub'] - assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '-')) == ['--bsub-opt'] - assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '')) == ['csub'] - assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '-')) == ['--csub-opt'] - assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '')) == [] + assert choices_without_help(cli, [], '-') == ['--cli-opt'] + assert choices_without_help(cli, [], '') == ['asub'] + assert choices_without_help(cli, ['asub'], '-') == ['--asub-opt'] + assert choices_without_help(cli, ['asub'], '') == ['bsub'] + assert choices_without_help(cli, ['asub', 'bsub'], '-') == ['--bsub-opt'] + assert choices_without_help(cli, ['asub', 'bsub'], '') == ['csub'] + assert choices_without_help(cli, ['asub', 'bsub', 'csub'], '-') == ['--csub-opt', '--csub', '--search-color'] + assert choices_without_help(cli, ['asub', 'bsub', 'csub', '--csub-opt'], '') == CSUB_OPT_CHOICES + assert choices_without_help(cli, ['asub', 'bsub', 'csub'], '--csub') == ['--csub-opt', '--csub'] + assert choices_without_help(cli, ['asub', 'bsub', 'csub', '--csub'], '') == CSUB_CHOICES + assert choices_without_help(cli, ['asub', 'bsub', 'csub', '--csub-opt'], 'f') == ['foo'] + assert choices_without_help(cli, ['asub', 'bsub', 'csub'], '') == COLORS + assert choices_without_help(cli, ['asub', 'bsub', 'csub'], 'b') == ['blue'] + assert choices_without_help(cli, ['asub', 'bsub', 'csub', '--search-color'], 'een') == ['green'] + + +def test_chaining(): + @click.group('cli', chain=True) + @click.option('--cli-opt') + def cli(cli_opt): + pass + + @cli.command() + @click.option('--asub-opt') + def asub(asub_opt): + pass + + @cli.command(help='bsub help') + @click.option('--bsub-opt') + @click.argument('arg', type=click.Choice(['arg1', 'arg2']), required=True) + def bsub(bsub_opt, arg): + pass + + @cli.command() + @click.option('--csub-opt') + @click.argument('arg', type=click.Choice(['carg1', 'carg2']), required=False) + def csub(csub_opt, arg): + pass + + assert choices_without_help(cli, [], '-') == ['--cli-opt'] + assert choices_without_help(cli, [], '') == ['asub', 'bsub', 'csub'] + assert choices_without_help(cli, ['asub'], '-') == ['--asub-opt'] + assert choices_without_help(cli, ['asub'], '') == ['bsub', 'csub'] + assert choices_without_help(cli, ['bsub'], '') == ['arg1', 'arg2'] + assert choices_without_help(cli, ['asub', '--asub-opt'], '') == [] + assert choices_without_help(cli, ['asub', '--asub-opt', '5', 'bsub'], '-') == ['--bsub-opt'] + assert choices_without_help(cli, ['asub', 'bsub'], '-') == ['--bsub-opt'] + assert choices_with_help(cli, ['asub'], 'b') == [('bsub', 'bsub help')] + assert choices_without_help(cli, ['asub', 'csub'], '-') == ['--csub-opt'] + + +def test_argument_choice(): + @click.command() + @click.argument('arg1', required=False, type=click.Choice(['arg11', 'arg12'])) + @click.argument('arg2', required=False, type=click.Choice(['arg21', 'arg22'])) + @click.argument('arg3', required=False, type=click.Choice(['arg', 'argument'])) + def cli(): + pass + + assert choices_without_help(cli, [], '') == ['arg11', 'arg12'] + assert choices_without_help(cli, [], 'arg') == ['arg11', 'arg12'] + assert choices_without_help(cli, ['arg11'], '') == ['arg21', 'arg22'] + assert choices_without_help(cli, ['arg12', 'arg21'], '') == ['arg', 'argument'] + assert choices_without_help(cli, ['arg12', 'arg21'], 'argu') == ['argument'] + + +def test_option_choice(): + @click.command() + @click.option('--opt1', type=click.Choice(['opt11', 'opt12']), help='opt1 help') + @click.option('--opt2', type=click.Choice(['opt21', 'opt22'])) + @click.option('--opt3', type=click.Choice(['opt', 'option'])) + def cli(): + pass + + assert choices_with_help(cli, [], '-') == [('--opt1', 'opt1 help'), + ('--opt2', None), + ('--opt3', None)] + assert choices_without_help(cli, [], '--opt') == ['--opt1', '--opt2', '--opt3'] + assert choices_without_help(cli, [], '--opt1=') == ['opt11', 'opt12'] + assert choices_without_help(cli, [], '--opt2=') == ['opt21', 'opt22'] + assert choices_without_help(cli, ['--opt2'], '=') == ['opt21', 'opt22'] + assert choices_without_help(cli, ['--opt2', '='], 'opt') == ['opt21', 'opt22'] + assert choices_without_help(cli, ['--opt1'], '') == ['opt11', 'opt12'] + assert choices_without_help(cli, ['--opt2'], '') == ['opt21', 'opt22'] + assert choices_without_help(cli, ['--opt1', 'opt11', '--opt2'], '') == ['opt21', 'opt22'] + assert choices_without_help(cli, ['--opt2', 'opt21'], '-') == ['--opt1', '--opt3'] + assert choices_without_help(cli, ['--opt1', 'opt11'], '-') == ['--opt2', '--opt3'] + assert choices_without_help(cli, ['--opt1'], 'opt') == ['opt11', 'opt12'] + assert choices_without_help(cli, ['--opt3'], 'opti') == ['option'] + + assert choices_without_help(cli, ['--opt1', 'invalid_opt'], '-') == ['--opt2', '--opt3'] + + +def test_option_and_arg_choice(): + @click.command() + @click.option('--opt1', type=click.Choice(['opt11', 'opt12'])) + @click.argument('arg1', required=False, type=click.Choice(['arg11', 'arg12'])) + @click.option('--opt2', type=click.Choice(['opt21', 'opt22'])) + def cli(): + pass + + assert choices_without_help(cli, ['--opt1'], '') == ['opt11', 'opt12'] + assert choices_without_help(cli, [''], '--opt1=') == ['opt11', 'opt12'] + assert choices_without_help(cli, [], '') == ['arg11', 'arg12'] + assert choices_without_help(cli, ['--opt2'], '') == ['opt21', 'opt22'] + + +def test_boolean_flag_choice(): + @click.command() + @click.option('--shout/--no-shout', default=False) + @click.argument('arg', required=False, type=click.Choice(['arg1', 'arg2'])) + def cli(local_opt): + pass + + assert choices_without_help(cli, [], '-') == ['--shout', '--no-shout'] + assert choices_without_help(cli, ['--shout'], '') == ['arg1', 'arg2'] + + +def test_multi_value_option_choice(): + @click.command() + @click.option('--pos', nargs=2, type=click.Choice(['pos1', 'pos2'])) + @click.argument('arg', required=False, type=click.Choice(['arg1', 'arg2'])) + def cli(local_opt): + pass + + assert choices_without_help(cli, ['--pos'], '') == ['pos1', 'pos2'] + assert choices_without_help(cli, ['--pos', 'pos1'], '') == ['pos1', 'pos2'] + assert choices_without_help(cli, ['--pos', 'pos1', 'pos2'], '') == ['arg1', 'arg2'] + assert choices_without_help(cli, ['--pos', 'pos1', 'pos2', 'arg1'], '') == [] + + +def test_multi_option_choice(): + @click.command() + @click.option('--message', '-m', multiple=True, type=click.Choice(['m1', 'm2'])) + @click.argument('arg', required=False, type=click.Choice(['arg1', 'arg2'])) + def cli(local_opt): + pass + + assert choices_without_help(cli, ['-m'], '') == ['m1', 'm2'] + assert choices_without_help(cli, ['-m', 'm1', '-m'], '') == ['m1', 'm2'] + assert choices_without_help(cli, ['-m', 'm1'], '') == ['arg1', 'arg2'] + + +def test_variadic_argument_choice(): + @click.command() + @click.argument('src', nargs=-1, type=click.Choice(['src1', 'src2'])) + def cli(local_opt): + pass + + assert choices_without_help(cli, ['src1', 'src2'], '') == ['src1', 'src2'] + + +def test_long_chain_choice(): + @click.group() + def cli(): + pass + + @cli.group() + @click.option('--sub-opt', type=click.Choice(['subopt1', 'subopt2'])) + @click.argument('sub-arg', required=False, type=click.Choice(['subarg1', 'subarg2'])) + def sub(sub_opt): + pass + + @sub.command(short_help='bsub help') + @click.option('--bsub-opt', type=click.Choice(['bsubopt1', 'bsubopt2'])) + @click.argument('bsub-arg1', required=False, type=click.Choice(['bsubarg1', 'bsubarg2'])) + @click.argument('bbsub-arg2', required=False, type=click.Choice(['bbsubarg1', 'bbsubarg2'])) + def bsub(bsub_opt): + pass + + assert choices_with_help(cli, ['sub'], '') == [('subarg1', None), ('subarg2', None), ('bsub', 'bsub help')] + assert choices_without_help(cli, ['sub', '--sub-opt'], '') == ['subopt1', 'subopt2'] + assert choices_without_help(cli, ['sub', '--sub-opt', 'subopt1'], '') == \ + ['subarg1', 'subarg2', 'bsub'] + assert choices_without_help(cli, + ['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub'], '-') == ['--bsub-opt'] + assert choices_without_help(cli, + ['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub', '--bsub-opt'], '') == \ + ['bsubopt1', 'bsubopt2'] + assert choices_without_help(cli, + ['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub', '--bsub-opt', 'bsubopt1', 'bsubarg1'], + '') == ['bbsubarg1', 'bbsubarg2'] diff --git a/tests/test_basic.py b/tests/test_basic.py index 045f608..8ba251f 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -180,6 +180,28 @@ def test_boolean_option(runner): assert result.output == '%s\n' % (default) +def test_boolean_conversion(runner): + for default in True, False: + @click.command() + @click.option('--flag', default=default, type=bool) + def cli(flag): + click.echo(flag) + + for value in 'true', 't', '1', 'yes', 'y': + result = runner.invoke(cli, ['--flag', value]) + assert not result.exception + assert result.output == 'True\n' + + for value in 'false', 'f', '0', 'no', 'n': + result = runner.invoke(cli, ['--flag', value]) + assert not result.exception + assert result.output == 'False\n' + + result = runner.invoke(cli, []) + assert not result.exception + assert result.output == '%s\n' % default + + def test_file_option(runner): @click.command() @click.option('--file', type=click.File('w')) @@ -343,6 +365,39 @@ def test_int_range_option(runner): assert result.output == '0\n' +def test_float_range_option(runner): + @click.command() + @click.option('--x', type=click.FloatRange(0, 5)) + def cli(x): + click.echo(x) + + result = runner.invoke(cli, ['--x=5.0']) + assert not result.exception + assert result.output == '5.0\n' + + result = runner.invoke(cli, ['--x=6.0']) + assert result.exit_code == 2 + assert 'Invalid value for "--x": 6.0 is not in the valid range of 0 to 5.\n' \ + in result.output + + @click.command() + @click.option('--x', type=click.FloatRange(0, 5, clamp=True)) + def clamp(x): + click.echo(x) + + result = runner.invoke(clamp, ['--x=5.0']) + assert not result.exception + assert result.output == '5.0\n' + + result = runner.invoke(clamp, ['--x=6.0']) + assert not result.exception + assert result.output == '5\n' + + result = runner.invoke(clamp, ['--x=-1.0']) + assert not result.exception + assert result.output == '0\n' + + def test_required_option(runner): @click.command() @click.option('--foo', required=True) @@ -357,7 +412,7 @@ def test_required_option(runner): def test_evaluation_order(runner): called = [] - def memo(ctx, value): + def memo(ctx, param, value): called.append(value) return value @@ -397,3 +452,47 @@ def test_evaluation_order(runner): 'normal1', 'missing', ] + + +def test_hidden_option(runner): + @click.command() + @click.option('--nope', hidden=True) + def cli(nope): + click.echo(nope) + + result = runner.invoke(cli, ['--help']) + assert result.exit_code == 0 + assert '--nope' not in result.output + + +def test_hidden_command(runner): + @click.group() + def cli(): + pass + + @cli.command(hidden=True) + def nope(): + pass + + result = runner.invoke(cli, ['--help']) + assert result.exit_code == 0 + assert 'nope' not in result.output + + +def test_hidden_group(runner): + @click.group() + def cli(): + pass + + @cli.group(hidden=True) + def subgroup(): + pass + + @subgroup.command() + def nope(): + pass + + result = runner.invoke(cli, ['--help']) + assert result.exit_code == 0 + assert 'subgroup' not in result.output + assert 'nope' not in result.output diff --git a/tests/test_commands.py b/tests/test_commands.py index 9b6a6fb..9189eca 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import re + import click +import pytest def test_other_command_invoke(runner): @@ -60,9 +62,9 @@ def test_auto_shorthelp(runner): result = runner.invoke(cli, ['--help']) assert re.search( r'Commands:\n\s+' - r'long\s+This is a long text that is too long to show\.\.\.\n\s+' + r'long\s+This is a long text that is too long to show as short help\.\.\.\n\s+' r'short\s+This is a short text\.\n\s+' - r'special_chars\s+Login and store the token in ~/.netrc\.\s*', + r'special-chars\s+Login and store the token in ~/.netrc\.\s*', result.output) is not None @@ -206,7 +208,7 @@ def test_other_command_invoke_with_defaults(runner): @click.option('--foo', type=click.INT, default=42) @click.pass_context def other_cmd(ctx, foo): - assert ctx.info_name == 'other_cmd' + assert ctx.info_name == 'other-cmd' click.echo(foo) result = runner.invoke(cli, []) @@ -253,3 +255,29 @@ def test_unprocessed_options(runner): 'Verbosity: 4', 'Args: -foo|-x|--muhaha|x|y|-x', ] + + +def test_deprecated_in_help_messages(runner): + @click.command(deprecated=True) + def cmd_with_help(): + """CLI HELP""" + pass + + result = runner.invoke(cmd_with_help, ['--help']) + assert '(DEPRECATED)' in result.output + + @click.command(deprecated=True) + def cmd_without_help(): + pass + + result = runner.invoke(cmd_without_help, ['--help']) + assert '(DEPRECATED)' in result.output + + +def test_deprecated_in_invocation(runner): + @click.command(deprecated=True) + def deprecated_cmd(): + debug() + + result = runner.invoke(deprecated_cmd) + assert 'DeprecationWarning:' in result.output diff --git a/tests/test_compat.py b/tests/test_compat.py index e4ecdc8..a7b05f1 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,4 +1,5 @@ import click +import pytest if click.__version__ >= '3.0': @@ -11,14 +12,14 @@ if click.__version__ >= '3.0': def cli(foo): click.echo(foo) - result = runner.invoke(cli, ['--foo', 'wat']) - assert result.exit_code == 0 - assert 'WAT' in result.output - assert 'Invoked legacy parameter callback' in result.output + with pytest.warns(Warning, match='Invoked legacy parameter callback'): + result = runner.invoke(cli, ['--foo', 'wat']) + assert result.exit_code == 0 + assert 'WAT' in result.output def test_bash_func_name(): from click._bashcomplete import get_completion_script - script = get_completion_script('foo-bar baz_blah', '_COMPLETE_VAR').strip() + script = get_completion_script('foo-bar baz_blah', '_COMPLETE_VAR', 'bash').strip() assert script.startswith('_foo_barbaz_blah_completion()') assert '_COMPLETE_VAR=complete $1' in script diff --git a/tests/test_context.py b/tests/test_context.py index cbf9b98..35933be 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -193,6 +193,7 @@ def test_close_before_pop(runner): @click.pass_context def cli(ctx): ctx.obj = 'test' + @ctx.call_on_close def foo(): assert click.get_current_context().obj == 'test' @@ -203,3 +204,55 @@ def test_close_before_pop(runner): assert not result.exception assert result.output == 'aha!\n' assert called == [True] + + +def test_make_pass_decorator_args(runner): + """ + Test to check that make_pass_decorator doesn't consume arguments based on + invocation order. + """ + class Foo(object): + title = 'foocmd' + + pass_foo = click.make_pass_decorator(Foo) + + @click.group() + @click.pass_context + def cli(ctx): + ctx.obj = Foo() + + @cli.command() + @click.pass_context + @pass_foo + def test1(foo, ctx): + click.echo(foo.title) + + @cli.command() + @pass_foo + @click.pass_context + def test2(ctx, foo): + click.echo(foo.title) + + result = runner.invoke(cli, ['test1']) + assert not result.exception + assert result.output == 'foocmd\n' + + result = runner.invoke(cli, ['test2']) + assert not result.exception + assert result.output == 'foocmd\n' + + +def test_exit_not_standalone(): + @click.command() + @click.pass_context + def cli(ctx): + ctx.exit(1) + + assert cli.main([], 'test_exit_not_standalone', standalone_mode=False) == 1 + + @click.command() + @click.pass_context + def cli(ctx): + ctx.exit(0) + + assert cli.main([], 'test_exit_not_standalone', standalone_mode=False) == 0 diff --git a/tests/test_formatting.py b/tests/test_formatting.py index e2f550e..2005f98 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -74,11 +74,11 @@ def test_wrapping_long_options_strings(runner): # 54 is chosen as a length where the second line is one character # 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) assert not result.exception 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', ' SIXTH', '', @@ -111,11 +111,11 @@ def test_wrapping_long_command_name(runner): """A command. """ - result = runner.invoke(cli, ['a_very_very_very_long', 'command', '--help'], + result = runner.invoke(cli, ['a-very-very-very-long', 'command', '--help'], terminal_width=54) assert not result.exception 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', ' SIXTH', '', @@ -145,3 +145,155 @@ def test_formatting_empty_help_lines(runner): 'Options:', ' --help Show this message and exit.', ] + + +def test_formatting_usage_error(runner): + @click.command() + @click.argument('arg') + def cmd(arg): + click.echo('arg:' + arg) + + result = runner.invoke(cmd, []) + assert result.exit_code == 2 + assert result.output.splitlines() == [ + 'Usage: cmd [OPTIONS] ARG', + 'Try "cmd --help" for help.', + '', + 'Error: Missing argument "ARG".' + ] + + +def test_formatting_usage_error_metavar_missing_arg(runner): + """ + :author: @r-m-n + Including attribution to #612 + """ + @click.command() + @click.argument('arg', metavar='metavar') + def cmd(arg): + pass + + result = runner.invoke(cmd, []) + assert result.exit_code == 2 + assert result.output.splitlines() == [ + 'Usage: cmd [OPTIONS] metavar', + 'Try "cmd --help" for help.', + '', + 'Error: Missing argument "metavar".' + ] + + +def test_formatting_usage_error_metavar_bad_arg(runner): + @click.command() + @click.argument('arg', type=click.INT, metavar='metavar') + def cmd(arg): + pass + + result = runner.invoke(cmd, ['3.14']) + assert result.exit_code == 2 + assert result.output.splitlines() == [ + 'Usage: cmd [OPTIONS] metavar', + 'Try "cmd --help" for help.', + '', + 'Error: Invalid value for "metavar": 3.14 is not a valid integer' + ] + + +def test_formatting_usage_error_nested(runner): + @click.group() + def cmd(): + pass + + @cmd.command() + @click.argument('bar') + def foo(bar): + click.echo('foo:' + bar) + + result = runner.invoke(cmd, ['foo']) + assert result.exit_code == 2 + assert result.output.splitlines() == [ + 'Usage: cmd foo [OPTIONS] BAR', + 'Try "cmd foo --help" for help.', + '', + 'Error: Missing argument "BAR".' + ] + + +def test_formatting_usage_error_no_help(runner): + @click.command(add_help_option=False) + @click.argument('arg') + def cmd(arg): + click.echo('arg:' + arg) + + result = runner.invoke(cmd, []) + assert result.exit_code == 2 + assert result.output.splitlines() == [ + 'Usage: cmd [OPTIONS] ARG', + '', + 'Error: Missing argument "ARG".' + ] + + +def test_formatting_usage_custom_help(runner): + @click.command(context_settings=dict(help_option_names=['--man'])) + @click.argument('arg') + def cmd(arg): + click.echo('arg:' + arg) + + result = runner.invoke(cmd, []) + assert result.exit_code == 2 + assert result.output.splitlines() == [ + 'Usage: cmd [OPTIONS] ARG', + 'Try "cmd --man" for help.', + '', + 'Error: Missing argument "ARG".' + ] + +def test_formatting_custom_type_metavar(runner): + class MyType(click.ParamType): + def get_metavar(self, param): + return "MY_TYPE" + + @click.command("foo") + @click.help_option() + @click.argument("param", type=MyType()) + def cmd(param): + pass + + result = runner.invoke(cmd, '--help') + assert not result.exception + assert result.output.splitlines() == [ + 'Usage: foo [OPTIONS] MY_TYPE', + '', + 'Options:', + ' --help Show this message and exit.' + ] + + +def test_truncating_docstring(runner): + @click.command() + @click.pass_context + def cli(ctx): + """First paragraph. + + This is a very long second + paragraph and not correctly + wrapped but it will be rewrapped. + \f + + :param click.core.Context ctx: Click context. + """ + + result = runner.invoke(cli, ['--help'], terminal_width=60) + assert not result.exception + assert result.output.splitlines() == [ + 'Usage: cli [OPTIONS]', + '', + ' First paragraph.', + '', + ' This is a very long second paragraph and not correctly', + ' wrapped but it will be rewrapped.', + '', + 'Options:', + ' --help Show this message and exit.', + ] diff --git a/tests/test_imports.py b/tests/test_imports.py index bc54533..f400fa8 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -32,7 +32,7 @@ click.echo(json.dumps(rv)) ALLOWED_IMPORTS = set([ 'weakref', 'os', 'struct', 'collections', 'sys', 'contextlib', 'functools', 'stat', 're', 'codecs', 'inspect', 'itertools', 'io', - 'threading', 'colorama', 'errno' + 'threading', 'colorama', 'errno', 'fcntl' ]) if WIN: @@ -48,7 +48,6 @@ def test_light_imports(): if sys.version_info[0] != 2: rv = rv.decode('utf-8') imported = json.loads(rv) - print(imported) for module in imported: if module == 'click' or module.startswith('click.'): diff --git a/tests/test_options.py b/tests/test_options.py index d196fe2..46a4adb 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -84,7 +84,7 @@ def test_counting(runner): assert result.output == 'verbosity=0\n' result = runner.invoke(cli, ['--help']) - assert re.search('-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']) @@ -174,6 +174,33 @@ def test_multiple_default_type(runner): 'two --arg2 4 four'.split()) assert not result.exception +def test_dynamic_default_help_unset(runner): + @click.command() + @click.option('--username', prompt=True, + default=lambda: os.environ.get('USER', ''), + show_default=True) + def cmd(username): + print("Hello,", username) + + result = runner.invoke(cmd, ['--help']) + assert result.exit_code == 0 + assert '--username' in result.output + assert 'lambda' not in result.output + assert '(dynamic)' in result.output + +def test_dynamic_default_help_text(runner): + @click.command() + @click.option('--username', prompt=True, + default=lambda: os.environ.get('USER', ''), + show_default='current user') + def cmd(username): + print("Hello,", username) + + result = runner.invoke(cmd, ['--help']) + assert result.exit_code == 0 + assert '--username' in result.output + assert 'lambda' not in result.output + assert '(current user)' in result.output def test_nargs_envvar(runner): @click.command() @@ -198,8 +225,32 @@ def test_nargs_envvar(runner): assert result.output == 'x|1\ny|2\n' +def test_show_envvar(runner): + @click.command() + @click.option('--arg1', envvar='ARG1', + show_envvar=True) + def cmd(arg): + pass + + result = runner.invoke(cmd, ['--help']) + assert not result.exception + assert 'ARG1' in result.output + + +def test_show_envvar_auto_prefix(runner): + @click.command() + @click.option('--arg1', show_envvar=True) + def cmd(arg): + pass + + result = runner.invoke(cmd, ['--help'], + auto_envvar_prefix='TEST') + assert not result.exception + assert 'TEST_ARG1' in result.output + + def test_custom_validation(runner): - def validate_pos_int(ctx, value): + def validate_pos_int(ctx, param, value): if value < 0: raise click.BadParameter('Value needs to be positive') return value @@ -255,8 +306,42 @@ def test_missing_choice(runner): result = runner.invoke(cmd) assert result.exit_code == 2 - assert 'Error: Missing option "--foo". Choose from foo, bar.' \ - in result.output + error, separator, choices = result.output.partition('Choose from') + assert 'Error: Missing option "--foo". ' in error + assert 'Choose from' in separator + assert 'foo' in choices + assert 'bar' in choices + + +def test_case_insensitive_choice(runner): + @click.command() + @click.option('--foo', type=click.Choice( + ['Orange', 'Apple'], case_sensitive=False)) + def cmd(foo): + click.echo(foo) + + result = runner.invoke(cmd, ['--foo', 'apple']) + assert result.exit_code == 0 + + result = runner.invoke(cmd, ['--foo', 'oRANGe']) + assert result.exit_code == 0 + + result = runner.invoke(cmd, ['--foo', 'Apple']) + assert result.exit_code == 0 + + @click.command() + @click.option('--foo', type=click.Choice(['Orange', 'Apple'])) + def cmd2(foo): + click.echo(foo) + + result = runner.invoke(cmd2, ['--foo', 'apple']) + assert result.exit_code == 2 + + result = runner.invoke(cmd2, ['--foo', 'oRANGe']) + assert result.exit_code == 2 + + result = runner.invoke(cmd2, ['--foo', 'Apple']) + assert result.exit_code == 0 def test_multiline_help(runner): @@ -278,7 +363,6 @@ def test_multiline_help(runner): assert ' i am' in out assert ' multiline' in out - def test_argument_custom_class(runner): class CustomArgument(click.Argument): def get_default(self, ctx): @@ -311,6 +395,35 @@ def test_option_custom_class(runner): assert 'you wont see me' not in result.output +def test_option_custom_class_reusable(runner): + """Ensure we can reuse a custom class option. See Issue #926""" + + class CustomOption(click.Option): + def get_help_record(self, ctx): + '''a dumb override of a help text for testing''' + return ('--help', 'I am a help text') + + # Assign to a variable to re-use the decorator. + testoption = click.option('--testoption', cls=CustomOption, help='you wont see me') + + @click.command() + @testoption + def cmd1(testoption): + click.echo(testoption) + + @click.command() + @testoption + def cmd2(testoption): + click.echo(testoption) + + # Both of the commands should have the --help option now. + for cmd in (cmd1, cmd2): + + result = runner.invoke(cmd, ['--help']) + assert 'I am a help text' in result.output + assert 'you wont see me' not in result.output + + def test_aliases_for_flags(runner): @click.command() @click.option('--warnings/--no-warnings', ' /-W', default=True) @@ -335,3 +448,29 @@ def test_aliases_for_flags(runner): assert result.output == 'False\n' result = runner.invoke(cli_alt, ['-w']) assert result.output == 'True\n' + +@pytest.mark.parametrize('option_args,expected', [ + (['--aggressive', '--all', '-a'], 'aggressive'), + (['--first', '--second', '--third', '-a', '-b', '-c'], 'first'), + (['--apple', '--banana', '--cantaloupe', '-a', '-b', '-c'], 'apple'), + (['--cantaloupe', '--banana', '--apple', '-c', '-b', '-a'], 'cantaloupe'), + (['-a', '-b', '-c'], 'a'), + (['-c', '-b', '-a'], 'c'), + (['-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): + + @click.command() + @click.option(*option_args, is_flag=True) + def cmd(**kwargs): + click.echo(str(kwargs[expected])) + + assert cmd.params[0].name == expected + + for form in option_args: + if form.startswith('-'): + result = runner.invoke(cmd, [form]) + assert result.output == 'True\n' diff --git a/tests/test_termui.py b/tests/test_termui.py index a03a297..9e855d3 100644 --- a/tests/test_termui.py +++ b/tests/test_termui.py @@ -1,14 +1,130 @@ import click +import time + + +class FakeClock(object): + def __init__(self): + self.now = time.time() + + def advance_time(self, seconds=1): + self.now += seconds + + def time(self): + return self.now def test_progressbar_strip_regression(runner, monkeypatch): + fake_clock = FakeClock() label = ' padded line' @click.command() def cli(): with click.progressbar(tuple(range(10)), label=label) as progress: for thing in progress: - pass + fake_clock.advance_time() + monkeypatch.setattr(time, 'time', fake_clock.time) monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: True) assert label in runner.invoke(cli, []).output + + +def test_progressbar_length_hint(runner, monkeypatch): + class Hinted(object): + def __init__(self, n): + self.items = list(range(n)) + + def __length_hint__(self): + return len(self.items) + + def __iter__(self): + return self + + def __next__(self): + if self.items: + return self.items.pop() + else: + raise StopIteration + + next = __next__ + + fake_clock = FakeClock() + + @click.command() + def cli(): + with click.progressbar(Hinted(10), label='test') as progress: + for thing in progress: + fake_clock.advance_time() + + 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_progressbar_hidden(runner, monkeypatch): + fake_clock = FakeClock() + label = 'whatever' + + @click.command() + def cli(): + with click.progressbar(tuple(range(10)), label=label) as progress: + for thing in progress: + fake_clock.advance_time() + + monkeypatch.setattr(time, 'time', fake_clock.time) + monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: False) + assert runner.invoke(cli, []).output == '' + + +def test_choices_list_in_prompt(runner, monkeypatch): + @click.command() + @click.option('-g', type=click.Choice(['none', 'day', 'week', 'month']), + prompt=True) + def cli_with_choices(g): + pass + + @click.command() + @click.option('-g', type=click.Choice(['none', 'day', 'week', 'month']), + prompt=True, show_choices=False) + def cli_without_choices(g): + pass + + result = runner.invoke(cli_with_choices, [], input='none') + assert '(none, day, week, month)' in result.output + + result = runner.invoke(cli_without_choices, [], input='none') + assert '(none, day, week, month)' not in result.output + + +def test_secho(runner): + with runner.isolation() as outstreams: + click.secho(None, nl=False) + bytes = outstreams[0].getvalue() + assert bytes == b'' + + +def test_progressbar_yields_all_items(runner): + with click.progressbar(range(3)) as progress: + assert len(list(progress)) == 3 + + +def test_progressbar_update(runner, monkeypatch): + fake_clock = FakeClock() + + @click.command() + def cli(): + with click.progressbar(range(4)) as progress: + for _ in progress: + fake_clock.advance_time() + print("") + + monkeypatch.setattr(time, 'time', fake_clock.time) + monkeypatch.setattr(click._termui_impl, 'isatty', lambda _: True) + output = runner.invoke(cli, []).output + + lines = [line for line in output.split('\n') if '[' in line] + + assert ' 25% 00:00:03' in lines[0] + assert ' 50% 00:00:02' in lines[1] + assert ' 75% 00:00:01' in lines[2] + assert '100% ' in lines[3] diff --git a/tests/test_testing.py b/tests/test_testing.py index 7fc284a..b8565ad 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -153,16 +153,34 @@ def test_exit_code_and_output_from_sys_exit(): click.echo('hello world') sys.exit('error') + @click.command() + @click.pass_context + def cli_string_ctx_exit(ctx): + click.echo('hello world') + ctx.exit('error') + @click.command() def cli_int(): click.echo('hello world') sys.exit(1) + @click.command() + @click.pass_context + def cli_int_ctx_exit(ctx): + click.echo('hello world') + ctx.exit(1) + @click.command() def cli_float(): click.echo('hello world') sys.exit(1.0) + @click.command() + @click.pass_context + def cli_float_ctx_exit(ctx): + click.echo('hello world') + ctx.exit(1.0) + @click.command() def cli_no_error(): click.echo('hello world') @@ -173,14 +191,26 @@ def test_exit_code_and_output_from_sys_exit(): assert result.exit_code == 1 assert result.output == 'hello world\nerror\n' + result = runner.invoke(cli_string_ctx_exit) + assert result.exit_code == 1 + assert result.output == 'hello world\nerror\n' + result = runner.invoke(cli_int) assert result.exit_code == 1 assert result.output == 'hello world\n' + result = runner.invoke(cli_int_ctx_exit) + assert result.exit_code == 1 + assert result.output == 'hello world\n' + result = runner.invoke(cli_float) assert result.exit_code == 1 assert result.output == 'hello world\n1.0\n' + result = runner.invoke(cli_float_ctx_exit) + assert result.exit_code == 1 + assert result.output == 'hello world\n1.0\n' + result = runner.invoke(cli_no_error) assert result.exit_code == 0 assert result.output == 'hello world\n' @@ -202,3 +232,58 @@ def test_env(): assert result.output == 'ENV=some_value\n' assert os.environ == env_orig + + +def test_stderr(): + @click.command() + def cli_stderr(): + click.echo("stdout") + click.echo("stderr", err=True) + + runner = CliRunner(mix_stderr=False) + + result = runner.invoke(cli_stderr) + + assert result.output == 'stdout\n' + assert result.stdout == 'stdout\n' + assert result.stderr == 'stderr\n' + + runner_mix = CliRunner(mix_stderr=True) + result_mix = runner_mix.invoke(cli_stderr) + + assert result_mix.output == 'stdout\nstderr\n' + assert result_mix.stdout == 'stdout\nstderr\n' + + with pytest.raises(ValueError): + 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.option('--foo', default='bar') + def cli_args(foo): + click.echo(foo) + + runner = CliRunner() + result = runner.invoke(cli_args, args=args) + assert result.exit_code == 0 + assert result.output == expected_output + + +def test_setting_prog_name_in_extra(): + @click.command() + def cli(): + click.echo("ok") + + runner = CliRunner() + result = runner.invoke(cli, prog_name="foobar") + assert not result.exception + assert result.output == 'ok\n' diff --git a/tests/test_utils.py b/tests/test_utils.py index 88923ad..4fd7cbb 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -10,13 +10,13 @@ from click._compat import WIN, PY2 def test_echo(runner): - with runner.isolation() as out: + with runner.isolation() as outstreams: click.echo(u'\N{SNOWMAN}') click.echo(b'\x44\x44') click.echo(42, nl=False) click.echo(b'a', nl=False) click.echo('\x1b[31mx\x1b[39m', nl=False) - bytes = out.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' # If we are in Python 2, we expect that writing bytes into a string io @@ -35,12 +35,12 @@ def test_echo(runner): def cli(): click.echo(b'\xf6') result = runner.invoke(cli, []) - assert result.output_bytes == b'\xf6\n' + assert result.stdout_bytes == b'\xf6\n' # Ensure we do not strip for bytes. - with runner.isolation() as out: + with runner.isolation() as outstreams: click.echo(bytearray(b'\x1b[31mx\x1b[39m'), nl=False) - assert out.getvalue() == b'\x1b[31mx\x1b[39m' + assert outstreams[0].getvalue() == b'\x1b[31mx\x1b[39m' def test_echo_custom_file(): @@ -146,14 +146,36 @@ def test_prompts_abort(monkeypatch, capsys): assert out == 'Password: \nScrew you.\n' +def _test_gen_func(): + yield 'a' + yield 'b' + yield 'c' + yield 'abc' + + @pytest.mark.skipif(WIN, reason='Different behavior on windows.') @pytest.mark.parametrize('cat', ['cat', 'cat ', 'cat ']) -def test_echo_via_pager(monkeypatch, capfd, cat): +@pytest.mark.parametrize('test', [ + # We need lambda here, because pytest will + # reuse the parameters, and then the generators + # are already used and will not yield anymore + ('just text\n', lambda: 'just text'), + ('iterable\n', lambda: ["itera", "ble"]), + ('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): monkeypatch.setitem(os.environ, 'PAGER', cat) monkeypatch.setattr(click._termui_impl, 'isatty', lambda x: True) - click.echo_via_pager('haha') + + expected_output = test[0] + test_input = test[1]() + + click.echo_via_pager(test_input) + out, err = capfd.readouterr() - assert out == 'haha\n' + assert out == expected_output @pytest.mark.skipif(WIN, reason='Test does not make sense on Windows.') @@ -268,9 +290,9 @@ def test_iter_keepopenfile(tmpdir): expected = list(map(str, range(10))) p = tmpdir.mkdir('testdir').join('testfile') p.write(os.linesep.join(expected)) - f = p.open() - for e_line, a_line in zip(expected, click.utils.KeepOpenFile(f)): - assert e_line == a_line.strip() + with p.open() as f: + for e_line, a_line in zip(expected, click.utils.KeepOpenFile(f)): + assert e_line == a_line.strip() @pytest.mark.xfail(WIN and not PY2, reason='God knows ...') @@ -278,6 +300,7 @@ def test_iter_lazyfile(tmpdir): expected = list(map(str, range(10))) p = tmpdir.mkdir('testdir').join('testfile') p.write(os.linesep.join(expected)) - f = p.open() - for e_line, a_line in zip(expected, click.utils.LazyFile(f.name)): - assert e_line == a_line.strip() + with p.open() as f: + with click.utils.LazyFile(f.name) as lf: + for e_line, a_line in zip(expected, lf): + assert e_line == a_line.strip() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..1f68f4d --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +[tox] +envlist = py{36,35,34,27,py} + +[testenv] +passenv = LANG +deps = + pytest + coverage + colorama +commands = coverage run -p -m pytest {posargs:tests} + +[testenv:docs-html] +deps = sphinx +commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html + +[testenv:docs-linkcheck] +deps = sphinx +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