From 21a68bd1776248ff071bbfc321d81e9f341b5250 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 5 Mar 2021 08:46:06 -0800 Subject: [PATCH 1/2] add pass_meta_key decorator --- CHANGES.rst | 3 +++ docs/api.rst | 3 +++ src/click/decorators.py | 36 ++++++++++++++++++++++++++++++++++++ tests/test_context.py | 23 +++++++++++++++++++++++ 4 files changed, 65 insertions(+) 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/src/click/decorators.py b/src/click/decorators.py index 5742f05e5e..d2fe1056f7 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,6 +9,8 @@ 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): """Marks a callback as wanting to receive the current context @@ -75,6 +78,39 @@ def new_func(*args, **kwargs): 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 + + def _make_command(f, name, attrs, cls): if isinstance(f, Command): raise TypeError("Attempted to convert a callback into a command twice.") 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 = [] From d2b315ae317d96860323fbed67c3736df7928ece Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 5 Mar 2021 08:47:37 -0800 Subject: [PATCH 2/2] typing for pass decorators --- docs/conf.py | 1 + src/click/decorators.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) 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 d2fe1056f7..43c8fa2974 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -12,7 +12,8 @@ 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. """ @@ -20,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. @@ -32,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 @@ -58,22 +61,25 @@ 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