From 051288502668d4d6d2e230fad1eab8a0ece56899 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 11 Nov 2025 18:35:25 +0100 Subject: [PATCH 1/3] implement final --- src/py_avro_schema/_alias.py | 6 +++++- tests/test_plain_class.py | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/py_avro_schema/_alias.py b/src/py_avro_schema/_alias.py index 6eb2d83..afad2b0 100644 --- a/src/py_avro_schema/_alias.py +++ b/src/py_avro_schema/_alias.py @@ -7,7 +7,7 @@ import dataclasses from collections import defaultdict -from typing import Annotated, Type, get_args, get_origin +from typing import Annotated, Final, Type, get_args, get_origin FQN = str """Fully qualified name for a Python type""" @@ -108,6 +108,10 @@ def get_field_aliases_and_actual_type(py_type: Type) -> tuple[list[str] | None, Check if a type contains an alias metadata via `Alias` or `Aliases` as metadata. It returns the eventual aliases and the type. """ + + if get_origin(py_type) is Final: + return [], get_args(py_type)[0] + # py_type is not annotated. It can't have aliases if get_origin(py_type) is not Annotated: return [], py_type diff --git a/tests/test_plain_class.py b/tests/test_plain_class.py index 4f9b32f..a90da2c 100644 --- a/tests/test_plain_class.py +++ b/tests/test_plain_class.py @@ -10,7 +10,7 @@ # specific language governing permissions and limitations under the License. import re -from typing import Annotated +from typing import Annotated, Final import pytest @@ -160,3 +160,22 @@ def test_type_aliases_future(): expected = {"fields": [{"name": "name", "type": "string"}], "name": "PyClass", "type": "record"} assert_schema(PyClass, expected) + + +def test_typing_final(): + + class PyType: + var: Final[str] + field: Final[dict[str, int]] + + def __init__(self): + self.var = "Hello World" + self.field = {"John": 123} + + expected = { + "fields": [{"name": "var", "type": "string"}, {"name": "field", "type": {"type": "map", "values": "long"}}], + "name": "PyType", + "type": "record", + } + + assert_schema(PyType, expected) From 0df2a3a863c7077ad4f8053bf361d4801abb85aa Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 11 Nov 2025 18:47:58 +0100 Subject: [PATCH 2/3] better implementation --- src/py_avro_schema/_alias.py | 6 +----- src/py_avro_schema/_schemas.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/py_avro_schema/_alias.py b/src/py_avro_schema/_alias.py index afad2b0..6eb2d83 100644 --- a/src/py_avro_schema/_alias.py +++ b/src/py_avro_schema/_alias.py @@ -7,7 +7,7 @@ import dataclasses from collections import defaultdict -from typing import Annotated, Final, Type, get_args, get_origin +from typing import Annotated, Type, get_args, get_origin FQN = str """Fully qualified name for a Python type""" @@ -108,10 +108,6 @@ def get_field_aliases_and_actual_type(py_type: Type) -> tuple[list[str] | None, Check if a type contains an alias metadata via `Alias` or `Aliases` as metadata. It returns the eventual aliases and the type. """ - - if get_origin(py_type) is Final: - return [], get_args(py_type)[0] - # py_type is not annotated. It can't have aliases if get_origin(py_type) is not Annotated: return [], py_type diff --git a/src/py_avro_schema/_schemas.py b/src/py_avro_schema/_schemas.py index e6a579f..3a4f41e 100644 --- a/src/py_avro_schema/_schemas.py +++ b/src/py_avro_schema/_schemas.py @@ -33,6 +33,7 @@ Annotated, Any, Dict, + Final, ForwardRef, List, Literal, @@ -378,6 +379,28 @@ def data(self, names: NamesType) -> JSONType: return self.literal_value_schema.data(names=names) +@register_schema +class FinalSchema(Schema): + """An Avro schema for Python ``typing.Final``""" + + def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Option = Option(0)): + """An Avro schema for Python ``typing.Final``""" + super().__init__(py_type, namespace, options) + py_type = _type_from_annotated(py_type) + real_type = get_args(py_type)[0] + self.real_schema = _schema_obj(real_type, namespace=namespace, options=options) + + def data(self, names: NamesType) -> JSONType: + """Return the schema data""" + return self.real_schema.data(names=names) + + @classmethod + def handles_type(cls, py_type: Type) -> bool: + """Whether this schema class can represent a given Python class""" + py_type = _type_from_annotated(py_type) + return get_origin(py_type) is Final + + @register_schema class DictAsJSONSchema(Schema): """An Avro string schema representing a Python Dict[str, Any] or List[Dict[str, Any]] assuming JSON serialization""" From 35c06fed58dd34e731a6cedc1f3a098bcf1b69a8 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 11 Nov 2025 19:02:39 +0100 Subject: [PATCH 3/3] better error handling --- src/py_avro_schema/_schemas.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/py_avro_schema/_schemas.py b/src/py_avro_schema/_schemas.py index 3a4f41e..e5c07d9 100644 --- a/src/py_avro_schema/_schemas.py +++ b/src/py_avro_schema/_schemas.py @@ -387,7 +387,10 @@ def __init__(self, py_type: Type, namespace: Optional[str] = None, options: Opti """An Avro schema for Python ``typing.Final``""" super().__init__(py_type, namespace, options) py_type = _type_from_annotated(py_type) - real_type = get_args(py_type)[0] + try: + real_type = get_args(py_type)[0] + except IndexError: + raise TypeError("Can't generate Avro schema from Python typing.Final without a type parameter") self.real_schema = _schema_obj(real_type, namespace=namespace, options=options) def data(self, names: NamesType) -> JSONType: @@ -398,7 +401,7 @@ def data(self, names: NamesType) -> JSONType: def handles_type(cls, py_type: Type) -> bool: """Whether this schema class can represent a given Python class""" py_type = _type_from_annotated(py_type) - return get_origin(py_type) is Final + return get_origin(py_type) is Final or py_type is Final @register_schema