Skip to content

Commit a6fea31

Browse files
authored
Merge pull request pallets#1807 from pallets/pass-meta
add pass_meta_key decorator
2 parents 1cb8609 + d2b315a commit a6fea31

5 files changed

Lines changed: 79 additions & 7 deletions

File tree

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ Unreleased
184184
return a path object instead of a string. :issue:`405`
185185
- ``TypeError`` is raised when parameter with ``multiple=True`` or
186186
``nargs > 1`` has non-iterable default. :issue:`1749`
187+
- Add a ``pass_meta_key`` decorator for passing a key from
188+
``Context.meta``. This is useful for extensions using ``meta`` to
189+
store information. :issue:`1739`
187190

188191

189192
Version 7.1.2

docs/api.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ Decorators
3131

3232
.. autofunction:: make_pass_decorator
3333

34+
.. autofunction:: click.decorators.pass_meta_key
35+
36+
3437
Utilities
3538
---------
3639

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"sphinx_issues",
2525
"sphinx_tabs.tabs",
2626
]
27+
autodoc_typehints = "description"
2728
intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
2829
issues_github_path = "pallets/click"
2930

src/click/decorators.py

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import inspect
2+
import typing as t
23
from functools import update_wrapper
34

45
from .core import Argument
@@ -8,19 +9,22 @@
89
from .globals import get_current_context
910
from .utils import echo
1011

12+
if t.TYPE_CHECKING:
13+
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
1114

12-
def pass_context(f):
15+
16+
def pass_context(f: "F") -> "F":
1317
"""Marks a callback as wanting to receive the current context
1418
object as first argument.
1519
"""
1620

1721
def new_func(*args, **kwargs):
1822
return f(get_current_context(), *args, **kwargs)
1923

20-
return update_wrapper(new_func, f)
24+
return update_wrapper(t.cast("F", new_func), f)
2125

2226

23-
def pass_obj(f):
27+
def pass_obj(f: "F") -> "F":
2428
"""Similar to :func:`pass_context`, but only pass the object on the
2529
context onwards (:attr:`Context.obj`). This is useful if that object
2630
represents the state of a nested system.
@@ -29,10 +33,12 @@ def pass_obj(f):
2933
def new_func(*args, **kwargs):
3034
return f(get_current_context().obj, *args, **kwargs)
3135

32-
return update_wrapper(new_func, f)
36+
return update_wrapper(t.cast("F", new_func), f)
3337

3438

35-
def make_pass_decorator(object_type, ensure=False):
39+
def make_pass_decorator(
40+
object_type: t.Type, ensure: bool = False
41+
) -> "t.Callable[[F], F]":
3642
"""Given an object type this creates a decorator that will work
3743
similar to :func:`pass_obj` but instead of passing the object of the
3844
current context, it will find the innermost context of type
@@ -55,23 +61,59 @@ def new_func(ctx, *args, **kwargs):
5561
remembered on the context if it's not there yet.
5662
"""
5763

58-
def decorator(f):
64+
def decorator(f: "F") -> "F":
5965
def new_func(*args, **kwargs):
6066
ctx = get_current_context()
67+
6168
if ensure:
6269
obj = ctx.ensure_object(object_type)
6370
else:
6471
obj = ctx.find_object(object_type)
72+
6573
if obj is None:
6674
raise RuntimeError(
6775
"Managed to invoke callback without a context"
6876
f" object of type {object_type.__name__!r}"
6977
" existing."
7078
)
79+
7180
return ctx.invoke(f, obj, *args, **kwargs)
7281

73-
return update_wrapper(new_func, f)
82+
return update_wrapper(t.cast("F", new_func), f)
83+
84+
return decorator
85+
86+
87+
def pass_meta_key(
88+
key: str, *, doc_description: t.Optional[str] = None
89+
) -> "t.Callable[[F], F]":
90+
"""Create a decorator that passes a key from
91+
:attr:`click.Context.meta` as the first argument to the decorated
92+
function.
93+
94+
:param key: Key in ``Context.meta`` to pass.
95+
:param doc_description: Description of the object being passed,
96+
inserted into the decorator's docstring. Defaults to "the 'key'
97+
key from Context.meta".
7498
99+
.. versionadded:: 8.0
100+
"""
101+
102+
def decorator(f: "F") -> "F":
103+
def new_func(*args, **kwargs):
104+
ctx = get_current_context()
105+
obj = ctx.meta[key]
106+
return ctx.invoke(f, obj, *args, **kwargs)
107+
108+
return update_wrapper(t.cast("F", new_func), f)
109+
110+
if doc_description is None:
111+
doc_description = f"the {key!r} key from :attr:`click.Context.meta`"
112+
113+
decorator.__doc__ = (
114+
f"Decorator that passes {doc_description} as the first argument"
115+
" to the decorated function."
116+
)
75117
return decorator
76118

77119

tests/test_context.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import click
66
from click.core import ParameterSource
7+
from click.decorators import pass_meta_key
78

89

910
def test_ensure_context_objects(runner):
@@ -151,6 +152,28 @@ def cli(ctx):
151152
runner.invoke(cli, [], catch_exceptions=False)
152153

153154

155+
def test_make_pass_meta_decorator(runner):
156+
@click.group()
157+
@click.pass_context
158+
def cli(ctx):
159+
ctx.meta["value"] = "good"
160+
161+
@cli.command()
162+
@pass_meta_key("value")
163+
def show(value):
164+
return value
165+
166+
result = runner.invoke(cli, ["show"], standalone_mode=False)
167+
assert result.return_value == "good"
168+
169+
170+
def test_make_pass_meta_decorator_doc():
171+
pass_value = pass_meta_key("value")
172+
assert "the 'value' key from :attr:`click.Context.meta`" in pass_value.__doc__
173+
pass_value = pass_meta_key("value", doc_description="the test value")
174+
assert "passes the test value" in pass_value.__doc__
175+
176+
154177
def test_context_pushing():
155178
rv = []
156179

0 commit comments

Comments
 (0)