Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ Decorators

.. autofunction:: make_pass_decorator

.. autofunction:: click.decorators.pass_meta_key


Utilities
---------

Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
56 changes: 49 additions & 7 deletions src/click/decorators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import inspect
import typing as t
from functools import update_wrapper

from .core import Argument
Expand All @@ -8,19 +9,22 @@
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.
"""

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.
Expand All @@ -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
Expand All @@ -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


Expand Down
23 changes: 23 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import click
from click.core import ParameterSource
from click.decorators import pass_meta_key


def test_ensure_context_objects(runner):
Expand Down Expand Up @@ -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 = []

Expand Down