diff --git a/CHANGES.rst b/CHANGES.rst index ac11bf3134..406088e07d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -184,6 +184,9 @@ Unreleased return a path object instead of a string. :issue:`405` - ``TypeError`` is raised when parameter with ``multiple=True`` or ``nargs > 1`` has non-iterable default. :issue:`1749` +- Add a ``pass_meta_key`` decorator for passing a key from + ``Context.meta``. This is useful for extensions using ``meta`` to + store information. :issue:`1739` Version 7.1.2 diff --git a/docs/api.rst b/docs/api.rst index 5e8c765c4b..23d6af34c7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -31,6 +31,9 @@ Decorators .. autofunction:: make_pass_decorator +.. autofunction:: click.decorators.pass_meta_key + + Utilities --------- diff --git a/docs/conf.py b/docs/conf.py index 63956c50d1..129f6341c1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,6 +24,7 @@ "sphinx_issues", "sphinx_tabs.tabs", ] +autodoc_typehints = "description" intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} issues_github_path = "pallets/click" diff --git a/src/click/decorators.py b/src/click/decorators.py index 5742f05e5e..43c8fa2974 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -1,4 +1,5 @@ import inspect +import typing as t from functools import update_wrapper from .core import Argument @@ -8,8 +9,11 @@ from .globals import get_current_context from .utils import echo +if t.TYPE_CHECKING: + F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -def pass_context(f): + +def pass_context(f: "F") -> "F": """Marks a callback as wanting to receive the current context object as first argument. """ @@ -17,10 +21,10 @@ def pass_context(f): def new_func(*args, **kwargs): return f(get_current_context(), *args, **kwargs) - return update_wrapper(new_func, f) + return update_wrapper(t.cast("F", new_func), f) -def pass_obj(f): +def pass_obj(f: "F") -> "F": """Similar to :func:`pass_context`, but only pass the object on the context onwards (:attr:`Context.obj`). This is useful if that object represents the state of a nested system. @@ -29,10 +33,12 @@ def pass_obj(f): def new_func(*args, **kwargs): return f(get_current_context().obj, *args, **kwargs) - return update_wrapper(new_func, f) + return update_wrapper(t.cast("F", new_func), f) -def make_pass_decorator(object_type, ensure=False): +def make_pass_decorator( + object_type: t.Type, ensure: bool = False +) -> "t.Callable[[F], F]": """Given an object type this creates a decorator that will work similar to :func:`pass_obj` but instead of passing the object of the current context, it will find the innermost context of type @@ -55,23 +61,59 @@ def new_func(ctx, *args, **kwargs): remembered on the context if it's not there yet. """ - def decorator(f): + def decorator(f: "F") -> "F": def new_func(*args, **kwargs): ctx = get_current_context() + if ensure: obj = ctx.ensure_object(object_type) else: obj = ctx.find_object(object_type) + if obj is None: raise RuntimeError( "Managed to invoke callback without a context" f" object of type {object_type.__name__!r}" " existing." ) + return ctx.invoke(f, obj, *args, **kwargs) - return update_wrapper(new_func, f) + return update_wrapper(t.cast("F", new_func), f) + + return decorator + + +def pass_meta_key( + key: str, *, doc_description: t.Optional[str] = None +) -> "t.Callable[[F], F]": + """Create a decorator that passes a key from + :attr:`click.Context.meta` as the first argument to the decorated + function. + + :param key: Key in ``Context.meta`` to pass. + :param doc_description: Description of the object being passed, + inserted into the decorator's docstring. Defaults to "the 'key' + key from Context.meta". + .. versionadded:: 8.0 + """ + + def decorator(f: "F") -> "F": + def new_func(*args, **kwargs): + ctx = get_current_context() + obj = ctx.meta[key] + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(t.cast("F", new_func), f) + + if doc_description is None: + doc_description = f"the {key!r} key from :attr:`click.Context.meta`" + + decorator.__doc__ = ( + f"Decorator that passes {doc_description} as the first argument" + " to the decorated function." + ) return decorator diff --git a/tests/test_context.py b/tests/test_context.py index c6aee3c09c..98f083570d 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -4,6 +4,7 @@ import click from click.core import ParameterSource +from click.decorators import pass_meta_key def test_ensure_context_objects(runner): @@ -151,6 +152,28 @@ def cli(ctx): runner.invoke(cli, [], catch_exceptions=False) +def test_make_pass_meta_decorator(runner): + @click.group() + @click.pass_context + def cli(ctx): + ctx.meta["value"] = "good" + + @cli.command() + @pass_meta_key("value") + def show(value): + return value + + result = runner.invoke(cli, ["show"], standalone_mode=False) + assert result.return_value == "good" + + +def test_make_pass_meta_decorator_doc(): + pass_value = pass_meta_key("value") + assert "the 'value' key from :attr:`click.Context.meta`" in pass_value.__doc__ + pass_value = pass_meta_key("value", doc_description="the test value") + assert "passes the test value" in pass_value.__doc__ + + def test_context_pushing(): rv = []