From 036bc230cec05b5f9f9bb0b118bf1b36d569e2ac Mon Sep 17 00:00:00 2001 From: faph Date: Tue, 3 Oct 2023 08:38:57 +0100 Subject: [PATCH 1/4] Reproducing reported issue of Pydantic inherited model failing --- tests/test_pydantic.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py index 2bc22e0..0fcb835 100644 --- a/tests/test_pydantic.py +++ b/tests/test_pydantic.py @@ -330,3 +330,21 @@ class PyType(pydantic.BaseModel): ], } assert_schema(PyType, expected) + + +def test_model_inheritance(): + class PyTypeCustomBase(pydantic.BaseModel): + field_a: str + + class PyType(PyTypeCustomBase): + field_b: str + + expected = { + "type": "record", + "name": "PyType", + "fields": [ + {"name": "field_a", "type": "string"}, + {"name": "field_b", "type": "string"}, + ], + } + assert_schema(PyType, expected) From 62489de0b26e5e87f8245891df0932f499138c87 Mon Sep 17 00:00:00 2001 From: faph Date: Tue, 3 Oct 2023 08:52:57 +0100 Subject: [PATCH 2/4] Add test for forward references in Pydantic base classes --- tests/test_pydantic.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py index 0fcb835..98629ab 100644 --- a/tests/test_pydantic.py +++ b/tests/test_pydantic.py @@ -343,8 +343,39 @@ class PyType(PyTypeCustomBase): "type": "record", "name": "PyType", "fields": [ - {"name": "field_a", "type": "string"}, - {"name": "field_b", "type": "string"}, + { + "name": "field_a", + "type": "string", + }, + { + "name": "field_b", + "type": "string", + }, + ], + } + assert_schema(PyType, expected) + + +@pytest.mark.xfail(reason="Forward references in base classes not supported yet") +def test_model_inheritance_self_ref_in_base(): + class PyTypeCustomBase(pydantic.BaseModel): + field_a: "PyTypeCustomBase" + + class PyType(PyTypeCustomBase): + field_b: str + + expected = { + "type": "record", + "name": "PyType", + "fields": [ + { + "name": "field_a", + "type": "PyTypeCustomBase", + }, + { + "name": "field_b", + "type": "string", + }, ], } assert_schema(PyType, expected) From eca520ac0a253b382e1fc0d43a7352fc7115ec28 Mon Sep 17 00:00:00 2001 From: faph Date: Tue, 3 Oct 2023 08:54:19 +0100 Subject: [PATCH 3/4] Do not use raw annotations for fields defined in Pydantic model base classes --- src/py_avro_schema/_schemas.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/py_avro_schema/_schemas.py b/src/py_avro_schema/_schemas.py index c07ac9d..ad3bf46 100644 --- a/src/py_avro_schema/_schemas.py +++ b/src/py_avro_schema/_schemas.py @@ -835,7 +835,9 @@ def _record_field(self, name: str, py_field: pydantic.fields.FieldInfo) -> Recor # Pydantic 2 resolves forward references for us. To avoid infinite recursion, we check if the unresolved raw # annoation is a forward reference. If so, we use that instead of Pydantic's resolved type hint. There might be # a better way to un-resolve the forward reference... - if isinstance(self.raw_annotations[name], (str, ForwardRef)): + # This does not support forward references from base model classes. If required we might need to traverse class + # hierarchy to get the raw annotations? + if isinstance(self.raw_annotations.get(name), (str, ForwardRef)): py_type = self.raw_annotations[name] else: py_type = py_field.annotation From 0c8b3ba21f50efa4e0884f2cf355088464cacc42 Mon Sep 17 00:00:00 2001 From: faph Date: Tue, 3 Oct 2023 09:13:50 +0100 Subject: [PATCH 4/4] Add section on forward refs in docs and explain pydantic forward ref in base classes issue --- docs/types.rst | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/types.rst b/docs/types.rst index e8bf24e..ea4cb14 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -140,6 +140,9 @@ To *disable* this, pass the option :attr:`py_avro_schema.Option.NO_DOC`. Recursive or repeated reference to the same Pydantic class is supported. After the first time the schema is output, any subsequent references are by name only. +.. warning:: + When using a hierarchy of Pydantic model classes, recursive type references are supported in the *final class only* and not in any inherited/base class. + Plain Python classes ~~~~~~~~~~~~~~~~~~~~ @@ -193,6 +196,39 @@ Union members can be any other type supported by **py-avro-schema**. When defined as a class field with a **default** value, the union members may be re-ordered to ensure that the first member matches the type of the default value. +Forward references +~~~~~~~~~~~~~~~~~~ + +Avro schema: any named schema + +**py-avro-schema** generally supports "forward" or recursive references, for example when a class attribute has the same +type as a the class itself. + +Example:: + + @dataclasses.dataclass + class PyType: + field_a: "PyType" + +Is output as: + +.. code-block:: json + + { + "type": "record", + "name": "PyType", + "fields": [ + { + "name": "field_a", + "type": "PyType", + }, + ], + } + +.. warning:: + When using a hierarchy of **Pydantic** model classes, recursive type references are supported in the *final class only* and not in any inherited/base class. + + Collections -----------