Repository: pallets/click (python)

Issue:
raise a more descriptive exception when @command decorator isn't called
Click 7.0, Python 3.7.4

Having done `pip install -e .` on my package to set up the CLI command, this error is raised because I decorated the main CLI function with `click.command` instead of `click.command()`:

```
Traceback (most recent call last):
  File "/Users/zev/.local/share/virtualenvs/myvirtualenvname-CI6hzx9c/bin/capture", line 11, in <module>
    load_entry_point('myvirtualenvname', 'console_scripts', 'my_module_name')()
TypeError: decorator() missing 1 required positional argument: 'f'
```

Would it be possible to raise a more informative exception referring to the decorator not having been called?

Pull request:
Add support for optional decorator parentheses
<!--
Before opening a PR, open a ticket describing the issue or feature the
PR will address. Follow the steps in CONTRIBUTING.rst.

Replace this comment with a description of the change. Describe how it
addresses the linked ticket.
-->

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to
automatically close an issue.
-->

- fixes #1359

<!--
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.
- [ ] 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 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))
 
 


Test patch:
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"