Repository: pallets/click (python)

Issue:
@pass_meta_key decorator
### Idea Proposal:

As a click's plugin developer, we use the `click.Context.meta` dictionary to store any state. So IMHO, click could provide another decorator: `@click.pass_metavar(key="foo")` similar to `@click.pass_obj`.

**Example implementation:**

```python
def pass_metavar(key: str):
    """Similar to :func:`click.pass_obj`,
    this passes the ctx.meta[key] variable instead as the first argument.
    """

    def decorator(f):
            @click.pass_context
            def new_func(ctx: click.Context, *args, **kwargs):
                var = ctx.meta[key]
                return ctx.invoke(f, var, *args, **kwargs)

        return update_wrapper(new_func, f)
    
    return decorator
```

**Example usage:**

```python
@click.pass_metavar(key="foo")
def bar(var):
    click.echo(var)
```

I think this would be beneficial for click's plugin developers and naturally anyone building a complex CLI app which requires using the `click.Context.meta` to store state. 

If you like the idea, I will be glad to raise a PR for it asap.

Pull request:
add pass_meta_key decorator
Extensions using `Context.meta` can use `pass_meta_key` to create decorators for passing their data to callbacks. The `doc_description` parameter can be used to add a specific description of the object, so that the decorator's docs can be included in the extension's docs.

```python
# in the extension
from click.decorators import pass_meta_key
pass_auth = pass_meta_key("click-auth.auth", doc_description="the :class:`ClickAuth` object")
```

```
# in the extension's docs
pass_auth.__doc__
Decorator that passes the :class:`ClickAuth` object as the first argument to the decorated function.
```

```python
# in the user code
@pass_auth
def list_prs(auth):
    ...
```

Also changed docs config so typing information is shown next to parameter descriptions instead of in the signature. https://pallets-click--1807.org.readthedocs.build/en/1807/api/#click.decorators.pass_meta_key

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to
automatically close an issue.
-->

- fixes #1739

<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to
each box below.

If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index ac11bf3..406088e 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 5e8c765..23d6af3 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 63956c5..129f634 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -24,6 +24,7 @@ extensions = [
     "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 5742f05..43c8fa2 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 .core import Option
 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 make_pass_decorator(object_type, ensure=False):
                    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
 
 


Test patch:
diff --git a/tests/test_context.py b/tests/test_context.py
index c6aee3c..98f0835 100644
--- a/tests/test_context.py
+++ b/tests/test_context.py
@@ -4,6 +4,7 @@ import pytest
 
 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 test_context_meta(runner):
     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 = []