diff --git a/CHANGES.rst b/CHANGES.rst index 01e8d8b15..2db769f1a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -78,6 +78,8 @@ Unreleased :issue:`2705` - Show correct value for flag default when using ``default_map``. :issue:`2632` +- Fix ``click.echo(color=...)`` passing ``color`` to coloroma so it can be + forced on Windows. :issue:`2606`. Version 8.1.7 diff --git a/docs/arguments.rst b/docs/arguments.rst index 60b53c558..5b877d82e 100644 --- a/docs/arguments.rst +++ b/docs/arguments.rst @@ -183,6 +183,9 @@ file in the same folder, and upon completion, the file will be moved over to the original location. This is useful if a file regularly read by other users is modified. + +.. _environment-variables: + Environment Variables --------------------- diff --git a/docs/commands.rst b/docs/commands.rst index 0cdc169f9..5fe24067f 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -275,9 +275,10 @@ When using chaining, there are a few restrictions: - The :attr:`Context.invoked_subcommand` attribute will be ``'*'`` because the parser doesn't know the full list of commands that will run yet. +.. _command-pipelines: Command Pipelines ------------------ +------------------ When using chaining, a common pattern is to have each command process the result of the previous command. diff --git a/docs/documentation.rst b/docs/documentation.rst index 349acfb83..da0aaa148 100644 --- a/docs/documentation.rst +++ b/docs/documentation.rst @@ -36,7 +36,7 @@ And what it looks like: .. _documenting-arguments: Documenting Arguments -~~~~~~~~~~~~~~~~~~~~~ +---------------------- :func:`click.argument` does not take a ``help`` parameter. This is to follow the general convention of Unix tools of using arguments for only diff --git a/docs/index.rst b/docs/index.rst index 4f3a4094d..d094efb3d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -69,6 +69,8 @@ usage patterns. why quickstart entry-points + virtualenv + setuptools parameters options arguments diff --git a/docs/parameters.rst b/docs/parameters.rst index b3604e750..7291a68c4 100644 --- a/docs/parameters.rst +++ b/docs/parameters.rst @@ -3,39 +3,60 @@ Parameters .. currentmodule:: click -Click supports two types of parameters for scripts: options and arguments. -There is generally some confusion among authors of command line scripts of -when to use which, so here is a quick overview of the differences. As its -name indicates, an option is optional. While arguments can be optional -within reason, they are much more restricted in how optional they can be. - -To help you decide between options and arguments, the recommendation is -to use arguments exclusively for things like going to subcommands or input -filenames / URLs, and have everything else be an option instead. - -Differences ------------ - -Arguments can do less than options. The following features are only -available for options: - -* automatic prompting for missing input -* act as flags (boolean or otherwise) -* option values can be pulled from environment variables, arguments can not -* options are fully documented in the help page, arguments are not - (:ref:`this is intentional ` as arguments - might be too specific to be automatically documented) - -On the other hand arguments, unlike options, can accept an arbitrary number -of arguments. Options can strictly ever only accept a fixed number of -arguments (defaults to 1), or they may be specified multiple times using -:ref:`multiple-options`. +Click supports only two types of parameters for scripts (by design): options and arguments. + +Options +---------------- + +* Are optional. +* Recommended to use for everything except subcommands, urls, or files. +* Can take a fixed number of arguments. The default is 1. They may be specified multiple times using :ref:`multiple-options`. +* Are fully documented by the help page. +* Have automatic prompting for missing input. +* Can act as flags (boolean or otherwise). +* Can be pulled from environment variables. + +Arguments +---------------- + +* Are optional with in reason, but not entirely so. +* Recommended to use for subcommands, urls, or files. +* Can take an arbitrary number of arguments. +* Are not fully documented by the help page since they may be too specific to be automatically documented. For more see :ref:`documenting-arguments`. +* Can be pulled from environment variables but only explicitly named ones. For more see :ref:`environment-variables`. + +.. _parameter_names: + +Parameter Names +--------------- + +Parameters (options and arguments) have a name that will be used as +the Python argument name when calling the decorated function with +values. + +.. click:example:: + + @click.command() + @click.argument('filename') + @click.option('-t', '--times', type=int) + def multi_echo(filename, times): + """Print value filename multiple times.""" + for x in range(times): + click.echo(filename) + +In the above example the argument's name is ``filename``. The name must match the python arg name. To provide a different name for use in help text, see :ref:`doc-meta-variables`. +The option's names are ``-t`` and ``--times``. More names are available for options and are covered in :ref:`options`. + +And what it looks like when run: + +.. click:run:: + + invoke(multi_echo, ['--times=3', 'index.txt'], prog_name='multi_echo') Parameter Types --------------- -Parameters can be of different types. Types can be implemented with -different behavior and some are supported out of the box: +The supported parameter types are: ``str`` / :data:`click.STRING`: The default parameter type which indicates unicode strings. @@ -74,37 +95,10 @@ different behavior and some are supported out of the box: .. autoclass:: DateTime :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) have a name that will be used as -the Python argument name when calling the decorated function with -values. - -Arguments take only one positional name. To provide a different name for -use in help text, see :ref:`doc-meta-variables`. - -Options can have many names that may be prefixed with one or two dashes. -Names with one dash are parsed as short options, names with two are -parsed as long options. If a name is not prefixed, it is used as the -Python argument name and not parsed as an option name. Otherwise, the -first name with a two dash prefix is used, or the first with a one dash -prefix if there are none with two. The prefix is removed and dashes are -converted to underscores to get the Python argument name. - - -Implementing Custom Types -------------------------- +How to Implement Custom Types +------------------------------- -To implement a custom type, you need to subclass the :class:`ParamType` -class. Override the :meth:`~ParamType.convert` method to convert the -value from a string to the correct type. +To implement a custom type, you need to subclass the :class:`ParamType` class. For simple cases, passing a Python function that fails with a `ValueError` is also supported, though discouraged. Override the :meth:`~ParamType.convert` method to convert the value from a string to the correct type. The following code implements an integer type that accepts hex and octal numbers in addition to normal integers, and converts them into regular diff --git a/docs/quickstart.rst b/docs/quickstart.rst index fbf9bd0cb..310e2b251 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,98 +3,28 @@ Quickstart .. currentmodule:: click -You can get the library directly from PyPI:: +Install +---------------------- +Install from PyPI:: pip install click -The installation into a :ref:`virtualenv` is heavily recommended. +Installing into a virtual environment is highly recommended. We suggest :ref:`virtualenv-heading`. -.. _virtualenv: - -virtualenv ----------- - -Virtualenv is probably what you want to use for developing Click -applications. - -What problem does virtualenv solve? Chances are that you want to use it -for other projects besides your Click script. But the more projects you -have, the more likely it is that you will be working with different -versions of Python itself, or at least different versions of Python -libraries. Let's face it: quite often libraries break backwards -compatibility, and it's unlikely that any serious application will have -zero dependencies. So what do you do if two or more of your projects have -conflicting dependencies? - -Virtualenv to the rescue! Virtualenv enables multiple side-by-side -installations of Python, one for each project. It doesn't actually -install separate copies of Python, but it does provide a clever way to -keep different project environments isolated. - -Create your project folder, then a virtualenv within it:: - - $ mkdir myproject - $ cd myproject - $ python3 -m venv .venv - -Now, whenever you want to work on a project, you only have to activate the -corresponding environment. On OS X and Linux, do the following:: - - $ . .venv/bin/activate - (venv) $ - -If you are a Windows user, the following command is for you:: - - > .venv\scripts\activate - (venv) > - -Either way, you should now be using your virtualenv (notice how the prompt of -your shell has changed to show the active environment). - -And if you want to stop using the virtualenv, use the following command:: - - $ deactivate - -After doing this, the prompt of your shell should be as familiar as before. - -Now, let's move on. Enter the following command to get Click activated in your -virtualenv:: - - $ pip install click - -A few seconds later and you are good to go. - -Screencast and Examples +Examples ----------------------- -There is a screencast available which shows the basic API of Click and -how to build simple applications with it. It also explores how to build -commands with subcommands. - -* `Building Command Line Applications with Click - `_ - -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``: `Color support demo - `_ -* ``termui``: `Terminal UI functions demo - `_ -* ``imagepipe``: `Command chaining demo - `_ +Some standalone examples of Click applications are packaged with Click. They are available in the `examples folder `_ of the repo. + +* `inout `_ : A very simple example of an application that can read from files and write to files and also accept input from stdin or write to stdout. +* `validation `_ : A simple example of an application that performs custom validation of parameters in different ways. +* `naval `_ : Port of the `docopt `_ naval example. +* `colors `_ : A simple example that colorizes text. Uses colorama on Windows. +* `aliases `_ : An advanced example that implements :ref:`aliases`. +* `imagepipe `_ : A complex example that implements some :ref:`command-pipelines` . It chains together image processing instructions. Requires pillow. +* `repo `_ : An advanced example that implements a Git-/Mercurial-like command line interface. +* `complex `_ : A very advanced example that implements loading subcommands dynamically from a plugin folder. +* `termui `_ : A simple example that showcases terminal UI helpers provided by click. Basic Concepts - Creating a Command ----------------------------------- diff --git a/docs/virtualenv.rst b/docs/virtualenv.rst new file mode 100644 index 000000000..2a565c102 --- /dev/null +++ b/docs/virtualenv.rst @@ -0,0 +1,54 @@ +.. _virtualenv-heading: + +Virtualenv +========================= + +Why Use Virtualenv? +------------------------- + +You should use `Virtualenv `_ because: + +* It allows you to install multiple versions of the same dependency. + +* If you have an operating system version of Python, it prevents you from changing its dependencies and potentially messing up your os. + +How to Use Virtualenv +----------------------------- + +Create your project folder, then a virtualenv within it:: + + $ mkdir myproject + $ cd myproject + $ python3 -m venv .venv + +Now, whenever you want to work on a project, you only have to activate the +corresponding environment. + +.. tabs:: + + .. group-tab:: OSX/Linux + + .. code-block:: text + + $ . .venv/bin/activate + (venv) $ + + .. group-tab:: Windows + + .. code-block:: text + + > .venv\scripts\activate + (venv) > + + +You are now using your virtualenv (notice how the prompt of your shell has changed to show the active environment). + +To install packages in the virtual environment:: + + $ pip install click + +And if you want to stop using the virtualenv, use the following command:: + + $ deactivate + +After doing this, the prompt of your shell should be as familiar as before. diff --git a/src/click/__init__.py b/src/click/__init__.py index 1aa547c57..f2360fa15 100644 --- a/src/click/__init__.py +++ b/src/click/__init__.py @@ -19,6 +19,7 @@ from .decorators import confirmation_option as confirmation_option from .decorators import group as group from .decorators import help_option as help_option +from .decorators import HelpOption as HelpOption from .decorators import make_pass_decorator as make_pass_decorator from .decorators import option as option from .decorators import pass_context as pass_context diff --git a/src/click/core.py b/src/click/core.py index bf8967f57..abe9fa9bb 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1446,11 +1446,11 @@ class Group(Command): ``chain`` is enabled. :param kwargs: Other arguments passed to :class:`Command`. - .. versionchanged:: 8.2 - Merged with and replaces the ``MultiCommand`` base class. - .. versionchanged:: 8.0 The ``commands`` argument can be a list of command objects. + + .. versionchanged:: 8.2 + Merged with and replaces the ``MultiCommand`` base class. """ allow_extra_args = True @@ -2629,7 +2629,9 @@ def _parse_decls( if name is None: if not expose_value: return None, opts, secondary_opts - raise TypeError("Could not determine name for option") + raise TypeError( + f"Could not determine name for option with declarations {decls!r}" + ) if not opts and not secondary_opts: raise TypeError( @@ -2993,7 +2995,7 @@ def _parse_decls( if not decls: if not expose_value: return None, [], [] - raise TypeError("Could not determine name for argument") + raise TypeError("Argument is marked as exposed, but does not have a name.") if len(decls) == 1: name = arg = decls[0] name = name.replace("-", "_").lower() diff --git a/src/click/decorators.py b/src/click/decorators.py index 3f77597ce..901f831ad 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -2,6 +2,7 @@ import inspect import typing as t +from collections import abc from functools import update_wrapper from gettext import gettext as _ @@ -524,32 +525,41 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None: return option(*param_decls, **kwargs) -def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: - """Add a ``--help`` option which immediately prints the help page +class HelpOption(Option): + """Pre-configured ``--help`` option which immediately prints the help page and exits the program. + """ - This is usually unnecessary, as the ``--help`` option is added to - each command automatically unless ``add_help_option=False`` is - passed. + def __init__( + self, + param_decls: abc.Sequence[str] | None = None, + **kwargs: t.Any, + ) -> None: + if not param_decls: + param_decls = ("--help",) - :param param_decls: One or more option names. Defaults to the single - value ``"--help"``. - :param kwargs: Extra arguments are passed to :func:`option`. - """ + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show this message and exit.")) + kwargs.setdefault("callback", self.show_help) - def callback(ctx: Context, param: Parameter, value: bool) -> None: - if not value or ctx.resilient_parsing: - return + super().__init__(param_decls, **kwargs) - echo(ctx.get_help(), color=ctx.color) - ctx.exit() + @staticmethod + def show_help(ctx: Context, param: Parameter, value: bool) -> None: + """Callback that print the help page on ```` and exits.""" + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() - if not param_decls: - param_decls = ("--help",) - kwargs.setdefault("is_flag", True) - kwargs.setdefault("expose_value", False) - kwargs.setdefault("is_eager", True) - kwargs.setdefault("help", _("Show this message and exit.")) - kwargs["callback"] = callback +def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Decorator for the pre-configured ``--help`` option defined above. + + :param param_decls: One or more option names. Defaults to the single + value ``"--help"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + kwargs.setdefault("cls", HelpOption) return option(*param_decls, **kwargs) diff --git a/src/click/exceptions.py b/src/click/exceptions.py index 27dd5e010..c41c20676 100644 --- a/src/click/exceptions.py +++ b/src/click/exceptions.py @@ -6,6 +6,7 @@ from gettext import ngettext from ._compat import get_text_stderr +from .globals import resolve_color_default from .utils import echo from .utils import format_filename @@ -30,6 +31,9 @@ class ClickException(Exception): def __init__(self, message: str) -> None: super().__init__(message) + # The context will be removed by the time we print the message, so cache + # the color settings here to be used later on (in `show`) + self.show_color: bool | None = resolve_color_default() self.message = message def format_message(self) -> str: @@ -42,7 +46,11 @@ def show(self, file: t.IO[t.Any] | None = None) -> None: if file is None: file = get_text_stderr() - echo(_("Error: {message}").format(message=self.format_message()), file=file) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=self.show_color, + ) class UsageError(ClickException): diff --git a/src/click/utils.py b/src/click/utils.py index 7ef90ddf9..ab2fe5889 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -314,7 +314,7 @@ def echo( out = strip_ansi(out) elif WIN: if auto_wrap_for_ansi is not None: - file = auto_wrap_for_ansi(file) # type: ignore + file = auto_wrap_for_ansi(file, color) # type: ignore elif not color: out = strip_ansi(out) diff --git a/tests/test_testing.py b/tests/test_testing.py index afb5ef9a2..0f2d512ab 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -5,6 +5,7 @@ import pytest import click +from click.exceptions import ClickException from click.testing import CliRunner @@ -199,6 +200,26 @@ def cli(): assert not result.exception +def test_with_color_errors(): + class CLIError(ClickException): + def format_message(self) -> str: + return click.style(self.message, fg="red") + + @click.command() + def cli(): + raise CLIError("Red error") + + runner = CliRunner() + + result = runner.invoke(cli) + assert result.output == "Error: Red error\n" + assert result.exception + + result = runner.invoke(cli, color=True) + assert result.output == f"Error: {click.style('Red error', fg='red')}\n" + assert result.exception + + def test_with_color_but_pause_not_blocking(): @click.command() def cli(): diff --git a/tests/test_utils.py b/tests/test_utils.py index b8b65e295..2ab175bec 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -36,9 +36,7 @@ def cli(): def test_echo_custom_file(): - import io - - f = io.StringIO() + f = StringIO() click.echo("hello", file=f) assert f.getvalue() == "hello\n" @@ -209,7 +207,6 @@ def test_echo_via_pager(monkeypatch, capfd, cat, test): assert out == expected_output -@pytest.mark.skipif(WIN, reason="Test does not make sense on Windows.") def test_echo_color_flag(monkeypatch, capfd): isatty = True monkeypatch.setattr(click._compat, "isatty", lambda x: isatty) @@ -232,9 +229,16 @@ def test_echo_color_flag(monkeypatch, capfd): assert out == f"{styled_text}\n" isatty = False - click.echo(styled_text) - out, err = capfd.readouterr() - assert out == f"{text}\n" + # Faking isatty() is not enough on Windows; + # the implementation caches the colorama wrapped stream + # so we have to use a new stream for each test + stream = StringIO() + click.echo(styled_text, file=stream) + assert stream.getvalue() == f"{text}\n" + + stream = StringIO() + click.echo(styled_text, file=stream, color=True) + assert stream.getvalue() == f"{styled_text}\n" def test_prompt_cast_default(capfd, monkeypatch):