From 10dd5a0e436a3bc3457745358ab570c545508934 Mon Sep 17 00:00:00 2001
From: Takeshi KOMIYA <i.tkomiya@gmail.com>
Date: Sat, 7 May 2022 19:52:25 +0900
Subject: [PATCH] Fix #10421: autodoc_preserve_defaults doesn't work on class
 methods

---
 CHANGES                                             |  2 ++
 sphinx/ext/autodoc/preserve_defaults.py             | 13 ++++++++++++-
 .../test-ext-autodoc/target/preserve_defaults.py    |  6 ++++++
 tests/test_ext_autodoc_preserve_defaults.py         |  9 +++++++++
 4 files changed, 29 insertions(+), 1 deletion(-)

diff --git a/CHANGES b/CHANGES
index 8eb829101c1..4ab2ddabea7 100644
--- a/CHANGES
+++ b/CHANGES
@@ -92,6 +92,8 @@ Bugs fixed
   function
 * #10305: autodoc: Failed to extract optional forward-ref'ed typehints correctly
   via :confval:`autodoc_type_aliases`
+* #10421: autodoc: :confval:`autodoc_preserve_defaults` doesn't work on class
+  methods
 * #10214: html: invalid language tag was generated if :confval:`language`
   contains a country code (ex. zh_CN)
 * #10236: html search: objects are duplicated in search result
diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py
index 6c4ee8f0926..93de022ddca 100644
--- a/sphinx/ext/autodoc/preserve_defaults.py
+++ b/sphinx/ext/autodoc/preserve_defaults.py
@@ -7,6 +7,7 @@
 import ast
 import inspect
 import sys
+from inspect import Parameter
 from typing import Any, Dict, List, Optional
 
 from sphinx.application import Sphinx
@@ -96,8 +97,18 @@ def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None:
                         if value is None:
                             value = ast_unparse(default)  # type: ignore
                         parameters[i] = param.replace(default=DefaultValue(value))
+
+            if bound_method and inspect.ismethod(obj):
+                # classmethods
+                cls = inspect.Parameter('cls', Parameter.POSITIONAL_OR_KEYWORD)
+                parameters.insert(0, cls)
+
             sig = sig.replace(parameters=parameters)
-            obj.__signature__ = sig
+            if bound_method and inspect.ismethod(obj):
+                # classmethods can't be assigned __signature__ attribute.
+                obj.__dict__['__signature__'] = sig
+            else:
+                obj.__signature__ = sig
     except (AttributeError, TypeError):
         # failed to update signature (ex. built-in or extension types)
         pass
diff --git a/tests/roots/test-ext-autodoc/target/preserve_defaults.py b/tests/roots/test-ext-autodoc/target/preserve_defaults.py
index fc83cf4fc3e..0cc3b4e2076 100644
--- a/tests/roots/test-ext-autodoc/target/preserve_defaults.py
+++ b/tests/roots/test-ext-autodoc/target/preserve_defaults.py
@@ -22,3 +22,9 @@ def meth(self, name: str = CONSTANT, sentinel: Any = SENTINEL,
              now: datetime = datetime.now(), color: int = 0xFFFFFF,
              *, kwarg1, kwarg2 = 0xFFFFFF) -> None:
         """docstring"""
+
+    @classmethod
+    def clsmeth(cls, name: str = CONSTANT, sentinel: Any = SENTINEL,
+                now: datetime = datetime.now(), color: int = 0xFFFFFF,
+                *, kwarg1, kwarg2 = 0xFFFFFF) -> None:
+        """docstring"""
diff --git a/tests/test_ext_autodoc_preserve_defaults.py b/tests/test_ext_autodoc_preserve_defaults.py
index fe62c929eec..ba5ff6e62da 100644
--- a/tests/test_ext_autodoc_preserve_defaults.py
+++ b/tests/test_ext_autodoc_preserve_defaults.py
@@ -28,6 +28,15 @@ def test_preserve_defaults(app):
         '   docstring',
         '',
         '',
+        '   .. py:method:: Class.clsmeth(name: str = CONSTANT, sentinel: ~typing.Any = '
+        'SENTINEL, now: ~datetime.datetime = datetime.now(), color: int = %s, *, '
+        'kwarg1, kwarg2=%s) -> None' % (color, color),
+        '      :module: target.preserve_defaults',
+        '      :classmethod:',
+        '',
+        '      docstring',
+        '',
+        '',
         '   .. py:method:: Class.meth(name: str = CONSTANT, sentinel: ~typing.Any = '
         'SENTINEL, now: ~datetime.datetime = datetime.now(), color: int = %s, *, '
         'kwarg1, kwarg2=%s) -> None' % (color, color),