Repository: samuelcolvin/pydantic (python)

Issue:
json_encoders inheritance
# Feature Request

Currently, defining `json_encoders` in child classes completely overrides the one defined from the parent classes:
```py
from datetime import datetime, timedelta
from pydantic import BaseModel

class A(BaseModel):
    dt: datetime = datetime.now()
    class Config:
        json_encoders = {datetime: lambda _: "encoded_from_A"}

print(A().json())
#> {"dt": "encoded_from_A"}
class B(A):
    timedt: timedelta = timedelta(hours=100)
    class Config:
        json_encoders = {timedelta: lambda _: "encoded_from_B"}

print(B().json())
#> {"dt": "2020-10-23T01:11:20.613624", "timedt": "encoded_from_B"}

```

I think it would be handy to merge the JSON encoders with the one defined in the parent ones, thus the result would be:
```py
print(B().json())
#> {"dt": "encoded_from_A", "timedt": "encoded_from_B"}
```

As an extension, it should still be possible to override the parent ones:
```py
class Bbis(A):
    timedt: timedelta = timedelta(hours=100)

    class Config:
        json_encoders = {
            datetime: lambda _: "encoded_from_Bbis",
            timedelta: lambda _: "encoded_from_Bbis",
        }

print(Bbis().json()) 
#> {"dt": "encoded_from_Bbis", "timedt": "encoded_from_Bbis"}
```

I'm eager to implement this feature if you think it's worth it :smile: 

Pull request:
Add merged json_encoders inheritance
## Change Summary

While inheriting the configuration , the `json_encoders` dictionaries are merged and the child classes can still override an encoder  defined in the parent class.

## Related issue number

Fixes #2024 

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)


Patch:
diff --git a/changes/2064-art049.md b/changes/2064-art049.md
new file mode 100644
index 000000000..426c3f379
--- /dev/null
+++ b/changes/2064-art049.md
@@ -0,0 +1,1 @@
+Add merged `json_encoders` inheritance
\ No newline at end of file
diff --git a/changes/2228-samuelcolvin.md b/changes/2228-samuelcolvin.md
new file mode 100644
index 000000000..05d4606f0
--- /dev/null
+++ b/changes/2228-samuelcolvin.md
@@ -0,0 +1,1 @@
+ensure cythonized functions are left untouched when creating models, based on #1944 by @kollmats
diff --git a/docs/examples/exporting_models_json_encoders_merge.py b/docs/examples/exporting_models_json_encoders_merge.py
new file mode 100644
index 000000000..b1562cdd0
--- /dev/null
+++ b/docs/examples/exporting_models_json_encoders_merge.py
@@ -0,0 +1,24 @@
+from datetime import datetime, timedelta
+from pydantic import BaseModel
+from pydantic.json import timedelta_isoformat
+
+
+class BaseClassWithEncoders(BaseModel):
+    dt: datetime
+    diff: timedelta
+
+    class Config:
+        json_encoders = {
+            datetime: lambda v: v.timestamp()
+        }
+
+
+class ChildClassWithEncoders(BaseClassWithEncoders):
+    class Config:
+        json_encoders = {
+            timedelta: timedelta_isoformat
+        }
+
+
+m = ChildClassWithEncoders(dt=datetime(2032, 6, 1), diff=timedelta(hours=100))
+print(m.json())
diff --git a/docs/usage/exporting_models.md b/docs/usage/exporting_models.md
index 26a6b0fa4..15cbd206b 100644
--- a/docs/usage/exporting_models.md
+++ b/docs/usage/exporting_models.md
@@ -99,11 +99,19 @@ _(This script is complete, it should run "as is")_
 By default, `timedelta` is encoded as a simple float of total seconds. The `timedelta_isoformat` is provided
 as an optional alternative which implements ISO 8601 time diff encoding.
 
+The `json_encoders` are also merged during the models inheritance with the child
+encoders taking precedence over the parent one.
+
+```py
+{!.tmp_examples/exporting_models_json_encoders_merge.py!}
+```
+_(This script is complete, it should run "as is")_
+
 ### Serialising subclasses
 
 !!! note
     New in version **v1.5**.
-    
+
     Subclasses of common types were not automatically serialised to JSON before **v1.5**.
 
 Subclasses of common types are automatically encoded like their super-classes:
diff --git a/pydantic/main.py b/pydantic/main.py
index c00cd1f7a..c2f8cb37f 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -169,13 +169,19 @@ class BaseConfig:
 
 
 def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType') -> 'ConfigType':
+    namespace = {}
     if not self_config:
         base_classes = (parent_config,)
     elif self_config == parent_config:
         base_classes = (self_config,)
     else:
         base_classes = self_config, parent_config  # type: ignore
-    return type('Config', base_classes, {})
+        namespace['json_encoders'] = {
+            **getattr(parent_config, 'json_encoders', {}),
+            **getattr(self_config, 'json_encoders', {}),
+        }
+
+    return type('Config', base_classes, namespace)
 
 
 EXTRA_LINK = 'https://pydantic-docs.helpmanual.io/usage/model_config/'
@@ -259,6 +265,11 @@ class ModelMetaclass(ABCMeta):
 
         prepare_config(config, name)
 
+        untouched_types = ANNOTATED_FIELD_UNTOUCHED_TYPES
+
+        def is_untouched(v: Any) -> bool:
+            return isinstance(v, untouched_types) or v.__class__.__name__ == 'cython_function_or_method'
+
         if (namespace.get('__module__'), namespace.get('__qualname__')) != ('pydantic.main', 'BaseModel'):
             annotations = resolve_annotations(namespace.get('__annotations__', {}), namespace.get('__module__', None))
             # annotation only fields need to come first in fields
@@ -270,7 +281,7 @@ class ModelMetaclass(ABCMeta):
                     value = namespace.get(ann_name, Undefined)
                     allowed_types = get_args(ann_type) if get_origin(ann_type) is Union else (ann_type,)
                     if (
-                        isinstance(value, ANNOTATED_FIELD_UNTOUCHED_TYPES)
+                        is_untouched(value)
                         and ann_type != PyObject
                         and not any(
                             lenient_issubclass(get_origin(allowed_type), Type) for allowed_type in allowed_types
@@ -289,7 +300,7 @@ class ModelMetaclass(ABCMeta):
 
             untouched_types = UNTOUCHED_TYPES + config.keep_untouched
             for var_name, value in namespace.items():
-                can_be_changed = var_name not in class_vars and not isinstance(value, untouched_types)
+                can_be_changed = var_name not in class_vars and not is_untouched(value)
                 if isinstance(value, ModelPrivateAttr):
                     if not is_valid_private_name(var_name):
                         raise NameError(
diff --git a/setup.cfg b/setup.cfg
index 0154d92e9..93865e164 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -4,6 +4,7 @@ addopts = -p no:hypothesispytest
 filterwarnings =
     error
     ignore::DeprecationWarning:distutils
+    ignore::DeprecationWarning:Cython
 
 [flake8]
 max-line-length = 120


Test patch:
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index 04d34aa60..5ad7347d1 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -20,6 +20,11 @@ from pydantic import (
 )
 from pydantic.fields import Field, Schema
 
+try:
+    import cython
+except ImportError:
+    cython = None
+
 
 def test_str_bytes():
     class Model(BaseModel):
@@ -1745,3 +1750,26 @@ def test_default_factory_validator_child():
         pass
 
     assert Child(foo=['a', 'b']).foo == ['a-1', 'b-1']
+
+
+@pytest.mark.skipif(cython is None, reason='cython not installed')
+def test_cython_function_untouched():
+    Model = cython.inline(
+        # language=Python
+        """
+from pydantic import BaseModel
+
+class Model(BaseModel):
+    a = 0.0
+    b = 10
+
+    def get_double_a(self) -> float:
+        return self.a + self.b
+
+return Model
+"""
+    )
+    model = Model(a=10.2)
+    assert model.a == 10.2
+    assert model.b == 10
+    return model.get_double_a() == 20.2
diff --git a/tests/test_json.py b/tests/test_json.py
index 07bfc04f7..06a4764d7 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -170,6 +170,35 @@ def test_custom_iso_timedelta():
     assert m.json() == '{"x": "P0DT0H2M3.000000S"}'
 
 
+def test_json_encoder_simple_inheritance():
+    class Parent(BaseModel):
+        dt: datetime.datetime = datetime.datetime.now()
+        timedt: datetime.timedelta = datetime.timedelta(hours=100)
+
+        class Config:
+            json_encoders = {datetime.datetime: lambda _: 'parent_encoder'}
+
+    class Child(Parent):
+        class Config:
+            json_encoders = {datetime.timedelta: lambda _: 'child_encoder'}
+
+    assert Child().json() == '{"dt": "parent_encoder", "timedt": "child_encoder"}'
+
+
+def test_json_encoder_inheritance_override():
+    class Parent(BaseModel):
+        dt: datetime.datetime = datetime.datetime.now()
+
+        class Config:
+            json_encoders = {datetime.datetime: lambda _: 'parent_encoder'}
+
+    class Child(Parent):
+        class Config:
+            json_encoders = {datetime.datetime: lambda _: 'child_encoder'}
+
+    assert Child().json() == '{"dt": "child_encoder"}'
+
+
 def test_custom_encoder_arg():
     class Model(BaseModel):
         x: datetime.timedelta