Repository: pallets/click

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.


Discussion:

Resolving patch:
diff --git a/docs/conf.py b/docs/conf.py
--- 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
--- 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
 
 


Test patch:
diff --git a/tests/test_context.py b/tests/test_context.py
--- 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 = []