diff options
-rw-r--r-- | CHANGES.rst | 2 | ||||
-rw-r--r-- | src/click/core.py | 32 | ||||
-rw-r--r-- | src/click/decorators.py | 58 | ||||
-rw-r--r-- | tests/test_command_decorators.py | 37 |
4 files changed, 124 insertions, 5 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 463a967..9d30887 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,8 @@ Unreleased locale detection is removed. :issue:`2198` - Single options boolean flags with ``show_default=True`` only show the default if it is ``True``. :issue:`1971` +- The ``command`` and ``group`` decorators can be applied with or + without parentheses. :issue:`1359` Version 8.0.4 diff --git a/src/click/core.py b/src/click/core.py index 95d852b..3d11ab5 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1797,7 +1797,7 @@ class Group(MultiCommand): def command( self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], Command]: + ) -> t.Union[t.Callable[[t.Callable[..., t.Any]], Command], Command]: """A shortcut decorator for declaring and attaching a command to the group. This takes the same arguments as :func:`command` and immediately registers the created command with this group by @@ -1806,6 +1806,9 @@ class Group(MultiCommand): To customize the command class used, set the :attr:`command_class` attribute. + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + .. versionchanged:: 8.0 Added the :attr:`command_class` attribute. """ @@ -1814,16 +1817,25 @@ class Group(MultiCommand): if self.command_class is not None and "cls" not in kwargs: kwargs["cls"] = self.command_class + func: t.Optional[t.Callable] = None + + if args and callable(args[0]): + func = args[0] + args = args[1:] + def decorator(f: t.Callable[..., t.Any]) -> Command: - cmd = command(*args, **kwargs)(f) + cmd: Command = command(*args, **kwargs)(f) self.add_command(cmd) return cmd + if func is not None: + return decorator(func) + return decorator def group( self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], "Group"]: + ) -> t.Union[t.Callable[[t.Callable[..., t.Any]], "Group"], "Group"]: """A shortcut decorator for declaring and attaching a group to the group. This takes the same arguments as :func:`group` and immediately registers the created group with this group by @@ -1832,11 +1844,20 @@ class Group(MultiCommand): To customize the group class used, set the :attr:`group_class` attribute. + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + .. versionchanged:: 8.0 Added the :attr:`group_class` attribute. """ from .decorators import group + func: t.Optional[t.Callable] = None + + if args and callable(args[0]): + func = args[0] + args = args[1:] + if self.group_class is not None and "cls" not in kwargs: if self.group_class is type: kwargs["cls"] = type(self) @@ -1844,10 +1865,13 @@ class Group(MultiCommand): kwargs["cls"] = self.group_class def decorator(f: t.Callable[..., t.Any]) -> "Group": - cmd = group(*args, **kwargs)(f) + cmd: Group = group(*args, **kwargs)(f) self.add_command(cmd) return cmd + if func is not None: + return decorator(func) + return decorator def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: diff --git a/src/click/decorators.py b/src/click/decorators.py index 7930a16..a8c6d2d 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -153,11 +153,29 @@ def _make_command( ) +@t.overload def command( name: t.Optional[str] = None, cls: t.Optional[t.Type[Command]] = None, **attrs: t.Any, ) -> t.Callable[[F], Command]: + ... + + +@t.overload +def command( + name: t.Callable, + cls: t.Optional[t.Type[Command]] = None, + **attrs: t.Any, +) -> Command: + ... + + +def command( + name: t.Union[str, t.Callable, None] = None, + cls: t.Optional[t.Type[Command]] = None, + **attrs: t.Any, +) -> t.Union[Command, t.Callable[[F], Command]]: r"""Creates a new :class:`Command` and uses the decorated function as callback. This will also automatically attach all decorated :func:`option`\s and :func:`argument`\s as parameters to the command. @@ -176,24 +194,62 @@ def command( name with underscores replaced by dashes. :param cls: the command class to instantiate. This defaults to :class:`Command`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. """ if cls is None: cls = Command + func: t.Optional[t.Callable] = None + + if callable(name): + func = name + name = None + def decorator(f: t.Callable[..., t.Any]) -> Command: cmd = _make_command(f, name, attrs, cls) # type: ignore cmd.__doc__ = f.__doc__ return cmd + if func is not None: + return decorator(func) + return decorator -def group(name: t.Optional[str] = None, **attrs: t.Any) -> t.Callable[[F], Group]: +@t.overload +def group( + name: t.Optional[str] = None, + **attrs: t.Any, +) -> t.Callable[[F], Group]: + ... + + +@t.overload +def group( + name: t.Callable, + **attrs: t.Any, +) -> Group: + ... + + +def group( + name: t.Union[str, t.Callable, None] = None, **attrs: t.Any +) -> t.Union[Group, t.Callable[[F], Group]]: """Creates a new :class:`Group` with a function as callback. This works otherwise the same as :func:`command` just that the `cls` parameter is set to :class:`Group`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. """ attrs.setdefault("cls", Group) + + if callable(name): + grp: t.Callable[[F], Group] = t.cast(Group, command(**attrs)) + return grp(name) + return t.cast(Group, command(name, **attrs)) diff --git a/tests/test_command_decorators.py b/tests/test_command_decorators.py new file mode 100644 index 0000000..8d07531 --- /dev/null +++ b/tests/test_command_decorators.py @@ -0,0 +1,37 @@ +import click + + +def test_command_no_parens(runner): + @click.command + def cli(): + click.echo("hello") + + result = runner.invoke(cli) + assert result.exception is None + assert result.output == "hello\n" + + +def test_group_no_parens(runner): + @click.group + def grp(): + click.echo("grp1") + + @grp.command + def cmd1(): + click.echo("cmd1") + + @grp.group + def grp2(): + click.echo("grp2") + + @grp2.command + def cmd2(): + click.echo("cmd2") + + result = runner.invoke(grp, ["cmd1"]) + assert result.exception is None + assert result.output == "grp1\ncmd1\n" + + result = runner.invoke(grp, ["grp2", "cmd2"]) + assert result.exception is None + assert result.output == "grp1\ngrp2\ncmd2\n" |