Repository: pallets/click (python)

Issue:
Cannot use parameterless command decorator with custom command_class

Click throws an assertion error if you try to use the parameterless command decorator on a group with a command_class defined. The error is due to the command_class being added to kwargs before the assertion for no kwargs.


# minimal repro
```
import click

class CustomCommand(click.Command):
    pass

class CustomGroup(click.Group):
    command_class = CustomCommand

@click.group(cls=CustomGroup)
def grp():
    pass

@grp.command
def cli():
    click.echo("hello custom command class")
```
## error

```
ewilliamson@ip-192-168-50-39 ~ % python minimal_repro.py
Traceback (most recent call last):
  File "/Users/ewilliamson/minimal_repro.py", line 14, in <module>
    def cli():
  File "/Users/ewilliamson/.pyenv/versions/3.10.7/lib/python3.10/site-packages/click/core.py", line 1847, in command
    assert (
AssertionError: Use 'command(**kwargs)(callable)' to provide arguments.
```

## expected
The `@grp.command` would be successfully parsed the same as without using `CustomGroup`

<!--
Describe the expected behavior that should have happened but didn't.
-->

Environment:

- Python version: `Python 3.10.7`
- Click version: `click==8.1.3`


Pull request:
Fix parameterless command with custom class
<!--
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.
-->

This reorders the setting of the `kwargs["cls"]` and `assert (
                len(args) == 1 and not kwargs
            )` to allow parameterless constructors even when `self.command_class` is defined.

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to
automatically close an issue.
-->

- fixes #2416 

<!--
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 37646a9..2debd78 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,6 +20,8 @@ Unreleased
 -   ZSH completion script works when loaded from ``fpath``. :issue:`2344`.
 -   ``EOFError`` and ``KeyboardInterrupt`` tracebacks are not suppressed when
     ``standalone_mode`` is disabled. :issue:`2380`
+-   ``@group.command`` does not fail if the group was created with a custom
+    ``command_class``. :issue:`2416`
 
 
 Version 8.1.3
diff --git a/src/click/core.py b/src/click/core.py
index a6fc501..b03a760 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -1871,9 +1871,6 @@ class Group(MultiCommand):
         """
         from .decorators import command
 
-        if self.command_class and kwargs.get("cls") is None:
-            kwargs["cls"] = self.command_class
-
         func: t.Optional[t.Callable[..., t.Any]] = None
 
         if args and callable(args[0]):
@@ -1883,6 +1880,9 @@ class Group(MultiCommand):
             (func,) = args
             args = ()
 
+        if self.command_class and kwargs.get("cls") is None:
+            kwargs["cls"] = self.command_class
+
         def decorator(f: t.Callable[..., t.Any]) -> Command:
             cmd: Command = command(*args, **kwargs)(f)
             self.add_command(cmd)


Test patch:
diff --git a/tests/test_command_decorators.py b/tests/test_command_decorators.py
index 9d54d7b..cfdab3c 100644
--- a/tests/test_command_decorators.py
+++ b/tests/test_command_decorators.py
@@ -11,6 +11,26 @@ def test_command_no_parens(runner):
     assert result.output == "hello\n"
 
 
+def test_custom_command_no_parens(runner):
+    class CustomCommand(click.Command):
+        pass
+
+    class CustomGroup(click.Group):
+        command_class = CustomCommand
+
+    @click.group(cls=CustomGroup)
+    def grp():
+        pass
+
+    @grp.command
+    def cli():
+        click.echo("hello custom command class")
+
+    result = runner.invoke(cli)
+    assert result.exception is None
+    assert result.output == "hello custom command class\n"
+
+
 def test_group_no_parens(runner):
     @click.group
     def grp():