Repository: pydantic/pydantic

Issue:
schema_extra should be allowed to be a classmethod
In my opinion, this check is unnecessarily restrictive:
https://github.com/samuelcolvin/pydantic/blob/938335c46f7e31da71e5c2a45eb3cf1ad51030b5/pydantic/schema.py#L470-L471
Also, it raises `TypeError` only for classmethods, but not for regular methods.



Discussion:
PR welcome to relax the check. 

Resolving patch:
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -21,6 +21,7 @@
     Union,
     cast,
     no_type_check,
+    overload,
 )
 
 from .class_validators import ROOT_KEY, ValidatorGroup, extract_root_validators, extract_validators, inherit_validators
@@ -43,6 +44,7 @@
 )
 
 if TYPE_CHECKING:
+    import typing_extensions
     from inspect import Signature
     from .class_validators import ValidatorListDict
     from .types import ModelOrDc
@@ -52,6 +54,16 @@
     ConfigType = Type['BaseConfig']
     Model = TypeVar('Model', bound='BaseModel')
 
+    class SchemaExtraCallable(typing_extensions.Protocol):
+        @overload
+        def __call__(self, schema: Dict[str, Any]) -> None:
+            pass
+
+        @overload  # noqa: F811
+        def __call__(self, schema: Dict[str, Any], model_class: Type['Model']) -> None:  # noqa: F811
+            pass
+
+
 try:
     import cython  # type: ignore
 except ImportError:
@@ -89,7 +101,7 @@ class BaseConfig:
     getter_dict: Type[GetterDict] = GetterDict
     alias_generator: Optional[Callable[[str], str]] = None
     keep_untouched: Tuple[type, ...] = ()
-    schema_extra: Union[Dict[str, Any], Callable[[Dict[str, Any]], None]] = {}
+    schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {}
     json_loads: Callable[[str], Any] = json.loads
     json_dumps: Callable[..., str] = json.dumps
     json_encoders: Dict[AnyType, AnyCallable] = {}
diff --git a/pydantic/schema.py b/pydantic/schema.py
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -5,7 +5,6 @@
 from enum import Enum
 from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
 from pathlib import Path
-from types import FunctionType
 from typing import (
     TYPE_CHECKING,
     Any,
@@ -467,8 +466,6 @@ def model_process_schema(
     s.update(m_schema)
     schema_extra = model.__config__.schema_extra
     if callable(schema_extra):
-        if not isinstance(schema_extra, FunctionType):
-            raise TypeError(f'{model.__name__}.Config.schema_extra callable is expected to be a staticmethod')
         if len(signature(schema_extra).parameters) == 1:
             schema_extra(s)
         else:


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1534,18 +1534,33 @@ def schema_extra(schema):
     assert Model.schema() == {'title': 'Model', 'type': 'override'}
 
 
-def test_model_with_schema_extra_callable_classmethod_asserts():
+def test_model_with_schema_extra_callable_classmethod():
     class Model(BaseModel):
         name: str = None
 
         class Config:
+            type = 'foo'
+
             @classmethod
             def schema_extra(cls, schema, model_class):
+                schema.pop('properties')
+                schema['type'] = cls.type
+                assert model_class is Model
+
+    assert Model.schema() == {'title': 'Model', 'type': 'foo'}
+
+
+def test_model_with_schema_extra_callable_instance_method():
+    class Model(BaseModel):
+        name: str = None
+
+        class Config:
+            def schema_extra(schema, model_class):
                 schema.pop('properties')
                 schema['type'] = 'override'
+                assert model_class is Model
 
-    with pytest.raises(TypeError, match='Model.Config.schema_extra callable is expected to be a staticmethod'):
-        Model.schema()
+    assert Model.schema() == {'title': 'Model', 'type': 'override'}
 
 
 def test_model_with_extra_forbidden():