localstack-py-avro-schema 3.9.0__tar.gz → 3.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/.github/workflows/test-package.yml +1 -3
  2. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/PKG-INFO +1 -1
  3. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/localstack_py_avro_schema.egg-info/PKG-INFO +1 -1
  4. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/localstack_py_avro_schema.egg-info/SOURCES.txt +1 -0
  5. localstack_py_avro_schema-3.9.2/src/py_avro_schema/_alias.py +116 -0
  6. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/py_avro_schema/_schemas.py +27 -5
  7. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tests/test_avro_schema.py +21 -0
  8. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tests/test_dataclass.py +26 -2
  9. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tests/test_plain_class.py +24 -1
  10. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tests/test_pydantic.py +19 -0
  11. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tests/test_typed_dict.py +29 -3
  12. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tox.ini +0 -4
  13. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/.editorconfig +0 -0
  14. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/.flake8 +0 -0
  15. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/.github/workflows/release-package.yml +0 -0
  16. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/.gitignore +0 -0
  17. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/.pre-commit-config.yaml +0 -0
  18. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/.readthedocs.yaml +0 -0
  19. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/LICENSE +0 -0
  20. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/LICENSE_HEADER.txt +0 -0
  21. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/README.md +0 -0
  22. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/docs/conf.py +0 -0
  23. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/docs/index.rst +0 -0
  24. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/docs/modules.rst +0 -0
  25. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/docs/py_avro_schema.rst +0 -0
  26. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/docs/tutorial.rst +0 -0
  27. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/docs/types.rst +0 -0
  28. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/pyproject.toml +0 -0
  29. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/setup.cfg +0 -0
  30. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/localstack_py_avro_schema.egg-info/dependency_links.txt +0 -0
  31. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/localstack_py_avro_schema.egg-info/requires.txt +0 -0
  32. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/localstack_py_avro_schema.egg-info/top_level.txt +0 -0
  33. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/py_avro_schema/__init__.py +0 -0
  34. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/py_avro_schema/_testing.py +0 -0
  35. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/py_avro_schema/_typing.py +0 -0
  36. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/src/py_avro_schema/py.typed +0 -0
  37. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tests/test_logicals.py +0 -0
  38. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tests/test_primitives.py +0 -0
  39. {localstack_py_avro_schema-3.9.0 → localstack_py_avro_schema-3.9.2}/tests/test_typing.py +0 -0
@@ -27,11 +27,9 @@ jobs:
27
27
  fail-fast: false
28
28
  matrix:
29
29
  python-version:
30
- - '3.9'
31
- - '3.10'
32
30
  - '3.11'
33
31
  - '3.12'
34
- - '3.13-dev'
32
+ - '3.13'
35
33
 
36
34
  steps:
37
35
  - uses: actions/checkout@v4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localstack-py-avro-schema
3
- Version: 3.9.0
3
+ Version: 3.9.2
4
4
  Summary: Generate Apache Avro schemas for Python types including standard library data-classes and Pydantic data models.
5
5
  Author-email: LocalStack Contributors <info@localstack.cloud>, "J.P. Morgan Chase & Co." <open_source@jpmorgan.com>
6
6
  License: Apache License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localstack-py-avro-schema
3
- Version: 3.9.0
3
+ Version: 3.9.2
4
4
  Summary: Generate Apache Avro schemas for Python types including standard library data-classes and Pydantic data models.
5
5
  Author-email: LocalStack Contributors <info@localstack.cloud>, "J.P. Morgan Chase & Co." <open_source@jpmorgan.com>
6
6
  License: Apache License
@@ -22,6 +22,7 @@ src/localstack_py_avro_schema.egg-info/dependency_links.txt
22
22
  src/localstack_py_avro_schema.egg-info/requires.txt
23
23
  src/localstack_py_avro_schema.egg-info/top_level.txt
24
24
  src/py_avro_schema/__init__.py
25
+ src/py_avro_schema/_alias.py
25
26
  src/py_avro_schema/_schemas.py
26
27
  src/py_avro_schema/_testing.py
27
28
  src/py_avro_schema/_typing.py
@@ -0,0 +1,116 @@
1
+ """
2
+ Module to register aliases for Python types
3
+
4
+ This module maintains global state via the _ALIASES registry.
5
+ Decorators will modify this state when applied to classes.
6
+ """
7
+
8
+ import dataclasses
9
+ from collections import defaultdict
10
+ from typing import Annotated, Type, get_args, get_origin
11
+
12
+ FQN = str
13
+ """Fully qualified name for a Python type"""
14
+ _ALIASES: dict[FQN, set[FQN]] = defaultdict(set)
15
+ """Maps the FQN of a Python type to a set of aliases"""
16
+
17
+
18
+ @dataclasses.dataclass
19
+ class Alias:
20
+ """Alias for a record field"""
21
+
22
+ alias: str
23
+
24
+
25
+ @dataclasses.dataclass
26
+ class Aliases:
27
+ """Aliases for a record field"""
28
+
29
+ aliases: list[str]
30
+
31
+
32
+ def get_fully_qualified_name(py_type: type) -> str:
33
+ """Returns the fully qualified name for a Python type"""
34
+ module = getattr(py_type, "__module__", None)
35
+ qualname = getattr(py_type, "__qualname__", py_type.__name__)
36
+
37
+ # py-avro-schema does not consider <locals> in the namespace.
38
+ # we skip it here as well for consistency
39
+ if module and "<locals>" in qualname:
40
+ return f"{module}.{py_type.__name__}"
41
+
42
+ if module and module not in ("builtins", "__main__"):
43
+ return f"{module}.{qualname}"
44
+ return qualname
45
+
46
+
47
+ def register_type_aliases(aliases: list[FQN]):
48
+ """
49
+ Decorator to register aliases for a given type.
50
+ It allows for compatible schemas following a change type (e.g., a rename), if the type fields do not
51
+ change in an incompatible way.
52
+
53
+ Example::
54
+ @register_type_aliases(aliases=["py_avro_schema.OldAddress"])
55
+ class Address(TypedDict):
56
+ street: str
57
+ number: int
58
+ """
59
+
60
+ def _wrapper(cls):
61
+ """Wrapper function that updates the aliases dictionary"""
62
+ fqn = get_fully_qualified_name(cls)
63
+ _ALIASES[fqn].update(aliases)
64
+ return cls
65
+
66
+ return _wrapper
67
+
68
+
69
+ def register_type_alias(alias: FQN):
70
+ """
71
+ Decorator to register a single alias for a given type.
72
+ It allows for compatible schemas following a change type (e.g., a rename), if the type fields do not
73
+ change in an incompatible way.
74
+
75
+ Example::
76
+ @register_type_alias(alias="py_avro_schema.OldAddress")
77
+ class Address(TypedDict):
78
+ street: str
79
+ number: int
80
+ """
81
+
82
+ def _wrapper(cls):
83
+ """Wrapper function that updates the aliases dictionary"""
84
+ fqn = get_fully_qualified_name(cls)
85
+ _ALIASES[fqn].add(alias)
86
+ return cls
87
+
88
+ return _wrapper
89
+
90
+
91
+ def get_aliases(fqn: str) -> list[str]:
92
+ """Returns the list of aliases for a given type"""
93
+ if aliases := _ALIASES.get(fqn):
94
+ return sorted(aliases)
95
+ return []
96
+
97
+
98
+ def get_field_aliases_and_actual_type(py_type: Type) -> tuple[list[str] | None, Type]:
99
+ """
100
+ Check if a type contains an alias metadata via `Alias` or `Aliases` as metadata.
101
+ It returns the eventual aliases and the type.
102
+ """
103
+ # py_type is not annotated. It can't have aliases
104
+ if get_origin(py_type) is not Annotated:
105
+ return [], py_type
106
+
107
+ args = get_args(py_type)
108
+ actual_type, annotation = args[0], args[1]
109
+
110
+ # Annotated type but not an alias. We do nothing.
111
+ if type(annotation) not in (Alias, Aliases):
112
+ return [], py_type
113
+
114
+ # If the annotated type is an alias, we extract the aliases and return the actual type
115
+ aliases = annotation.aliases if type(annotation) is Aliases else [annotation.alias]
116
+ return aliases, actual_type
@@ -50,6 +50,7 @@ import orjson
50
50
  import typeguard
51
51
 
52
52
  import py_avro_schema._typing
53
+ from py_avro_schema._alias import get_aliases, get_field_aliases_and_actual_type
53
54
 
54
55
  if TYPE_CHECKING:
55
56
  # Pydantic not necessarily required at runtime
@@ -857,6 +858,9 @@ class EnumSchema(NamedSchema):
857
858
  }
858
859
  if self.namespace is not None:
859
860
  enum_schema["namespace"] = self.namespace
861
+ fqn = f"{self.namespace}.{self.name}"
862
+ if aliases := get_aliases(fqn):
863
+ enum_schema["aliases"] = aliases
860
864
  if Option.NO_DOC not in self.options:
861
865
  doc = _doc_for_class(self.py_type)
862
866
  if doc:
@@ -887,6 +891,9 @@ class RecordSchema(NamedSchema):
887
891
  }
888
892
  if self.namespace is not None:
889
893
  record_schema["namespace"] = self.namespace
894
+ fqn = f"{self.namespace}.{self.name}"
895
+ if aliases := get_aliases(fqn):
896
+ record_schema["aliases"] = aliases
890
897
  if Option.NO_DOC not in self.options:
891
898
  doc = _doc_for_class(self.py_type)
892
899
  if doc:
@@ -902,6 +909,7 @@ class RecordField:
902
909
  py_type: Type,
903
910
  name: str,
904
911
  namespace: Optional[str],
912
+ aliases: list[str] | None = None,
905
913
  default: Any = dataclasses.MISSING,
906
914
  docs: str = "",
907
915
  options: Option = Option(0),
@@ -912,12 +920,16 @@ class RecordField:
912
920
  :param py_type: The Python class or type
913
921
  :param name: Field name
914
922
  :param namespace: Avro schema namespace
923
+ :param aliases: Aliases for the field
915
924
  :param default: Field default value
916
925
  :param docs: Field documentation or description
917
926
  :param options: Schema generation options
918
927
  """
928
+ if aliases is None:
929
+ aliases = []
919
930
  self.py_type = py_type
920
931
  self.name = name
932
+ self.aliases = aliases
921
933
  self._namespace = namespace
922
934
  self.default = default
923
935
  self.docs = docs
@@ -942,6 +954,8 @@ class RecordField:
942
954
  "name": self.name,
943
955
  "type": self.schema.data(names=names),
944
956
  }
957
+ if self.aliases:
958
+ field_data["aliases"] = sorted(self.aliases)
945
959
  if self.default != dataclasses.MISSING:
946
960
  field_data["default"] = self.schema.make_default(self.default)
947
961
  if self.docs and Option.NO_DOC not in self.options:
@@ -977,11 +991,13 @@ class DataclassSchema(RecordSchema):
977
991
  default = py_field.default
978
992
  if callable(py_field.default_factory): # type: ignore
979
993
  default = py_field.default_factory() # type: ignore
994
+ aliases, actual_type = get_field_aliases_and_actual_type(py_field.type) # type: ignore
980
995
  field_obj = RecordField(
981
- py_type=py_field.type, # type: ignore
996
+ py_type=actual_type, # type: ignore
982
997
  name=py_field.name,
983
998
  namespace=self.namespace_override,
984
999
  default=default,
1000
+ aliases=aliases,
985
1001
  options=self.options,
986
1002
  )
987
1003
 
@@ -1017,11 +1033,13 @@ class PydanticSchema(RecordSchema):
1017
1033
  default = dataclasses.MISSING if py_field.is_required() else py_field.get_default(call_default_factory=True)
1018
1034
  py_type = self._annotation(name)
1019
1035
  record_name = py_field.alias if Option.USE_FIELD_ALIAS in self.options and py_field.alias else name
1036
+ aliases, actual_type = get_field_aliases_and_actual_type(py_type)
1020
1037
  field_obj = RecordField(
1021
- py_type=py_type,
1038
+ py_type=actual_type,
1022
1039
  name=record_name,
1023
1040
  namespace=self.namespace_override,
1024
1041
  default=default,
1042
+ aliases=aliases,
1025
1043
  docs=py_field.description or "",
1026
1044
  options=self.options,
1027
1045
  )
@@ -1080,11 +1098,13 @@ class PlainClassSchema(RecordSchema):
1080
1098
  def _record_field(self, py_field: inspect.Parameter) -> RecordField:
1081
1099
  """Return an Avro record field object for a given Python instance attribute"""
1082
1100
  default = py_field.default if py_field.default != inspect.Parameter.empty else dataclasses.MISSING
1101
+ aliases, actual_type = get_field_aliases_and_actual_type(py_field.annotation)
1083
1102
  field_obj = RecordField(
1084
- py_type=py_field.annotation,
1103
+ py_type=actual_type,
1085
1104
  name=py_field.name,
1086
1105
  namespace=self.namespace_override,
1087
1106
  default=default,
1107
+ aliases=aliases,
1088
1108
  options=self.options,
1089
1109
  )
1090
1110
  return field_obj
@@ -1109,15 +1129,17 @@ class TypedDictSchema(RecordSchema):
1109
1129
  """
1110
1130
  super().__init__(py_type, namespace=namespace, options=options)
1111
1131
  py_type = _type_from_annotated(py_type)
1112
- self.py_fields: dict[str, Type] = get_type_hints(py_type)
1132
+ self.py_fields: dict[str, Type] = get_type_hints(py_type, include_extras=True)
1113
1133
  self.record_fields = [self._record_field(field) for field in self.py_fields.items()]
1114
1134
 
1115
1135
  def _record_field(self, py_field: tuple[str, Type]) -> RecordField:
1116
1136
  """Return an Avro record field object for a given TypedDict field"""
1137
+ aliases, actual_type = get_field_aliases_and_actual_type(py_field[1])
1117
1138
  field_obj = RecordField(
1118
- py_type=py_field[1],
1139
+ py_type=actual_type,
1119
1140
  name=py_field[0],
1120
1141
  namespace=self.namespace_override,
1142
+ aliases=aliases,
1121
1143
  options=self.options,
1122
1144
  )
1123
1145
  return field_obj
@@ -10,11 +10,14 @@
10
10
  # specific language governing permissions and limitations under the License.
11
11
 
12
12
  import dataclasses
13
+ import json
14
+ from typing import TypedDict
13
15
 
14
16
  import avro.schema
15
17
  import orjson
16
18
 
17
19
  import py_avro_schema as pas
20
+ from py_avro_schema._alias import register_type_alias, register_type_aliases
18
21
 
19
22
 
20
23
  def test_package_has_version():
@@ -43,3 +46,21 @@ def test_dataclass_string_field():
43
46
  json_data = pas.generate(PyType)
44
47
  assert json_data == orjson.dumps(expected)
45
48
  assert avro.schema.parse(json_data)
49
+
50
+
51
+ def test_avro_type_aliases():
52
+ @register_type_aliases(aliases=["test_avro_schema.VeryOldDict", "test_avro_schema.OldDict"])
53
+ class PyTypedDict(TypedDict):
54
+ value: str
55
+
56
+ json_data = pas.generate(PyTypedDict)
57
+ assert json.loads(json_data)["aliases"] == ["test_avro_schema.OldDict", "test_avro_schema.VeryOldDict"]
58
+
59
+ register_type_alias(alias="test_avro_schema.SuperOldDict")(PyTypedDict)
60
+ pas.generate.cache_clear()
61
+ json_data = pas.generate(PyTypedDict)
62
+ assert json.loads(json_data)["aliases"] == [
63
+ "test_avro_schema.OldDict",
64
+ "test_avro_schema.SuperOldDict",
65
+ "test_avro_schema.VeryOldDict",
66
+ ]
@@ -19,6 +19,7 @@ from typing import Annotated, Dict, List, Optional, Tuple
19
19
  import pytest
20
20
 
21
21
  import py_avro_schema as pas
22
+ from py_avro_schema._alias import Alias, register_type_aliases
22
23
  from py_avro_schema._testing import assert_schema
23
24
 
24
25
 
@@ -400,6 +401,7 @@ def test_dataclass_repeated_string_field():
400
401
 
401
402
 
402
403
  def test_dataclass_repeated_enum_field():
404
+ @register_type_aliases(aliases=["test_dataclass.OldEnum"])
403
405
  class PyTypeEnum(enum.Enum):
404
406
  RED = "RED"
405
407
  GREEN = "GREEN"
@@ -412,12 +414,15 @@ def test_dataclass_repeated_enum_field():
412
414
  expected = {
413
415
  "type": "record",
414
416
  "name": "PyType",
417
+ "namespace": "test_dataclass",
415
418
  "fields": [
416
419
  {
417
420
  "name": "field_child_1",
418
421
  "type": {
419
422
  "type": "enum",
420
423
  "name": "PyTypeEnum",
424
+ "namespace": "test_dataclass",
425
+ "aliases": ["test_dataclass.OldEnum"],
421
426
  "symbols": [
422
427
  "RED",
423
428
  "GREEN",
@@ -427,11 +432,11 @@ def test_dataclass_repeated_enum_field():
427
432
  },
428
433
  {
429
434
  "name": "field_child_2",
430
- "type": "PyTypeEnum",
435
+ "type": "test_dataclass.PyTypeEnum",
431
436
  },
432
437
  ],
433
438
  }
434
- assert_schema(PyType, expected)
439
+ assert_schema(PyType, expected, do_auto_namespace=True)
435
440
 
436
441
 
437
442
  def test_self_ref_field():
@@ -839,3 +844,22 @@ def test_sequence_schema_defaults_with_items():
839
844
  "type": "record",
840
845
  }
841
846
  assert_schema(PyType, expected)
847
+
848
+
849
+ def test_field_alias():
850
+ @dataclasses.dataclass
851
+ class PyType:
852
+ field_b: Annotated[str, Alias("field_a")]
853
+
854
+ expected = {
855
+ "type": "record",
856
+ "name": "PyType",
857
+ "fields": [
858
+ {
859
+ "aliases": ["field_a"],
860
+ "name": "field_b",
861
+ "type": "string",
862
+ }
863
+ ],
864
+ }
865
+ assert_schema(PyType, expected)
@@ -15,10 +15,12 @@ from typing import Annotated
15
15
  import pytest
16
16
 
17
17
  import py_avro_schema
18
+ from py_avro_schema._alias import Alias, register_type_aliases
18
19
  from py_avro_schema._testing import assert_schema
19
20
 
20
21
 
21
22
  def test_plain_class_with_type_hints():
23
+ @register_type_aliases(aliases=["test_plain_class.OldPyType"])
22
24
  class PyType:
23
25
  """A port, not using dataclass"""
24
26
 
@@ -33,6 +35,8 @@ def test_plain_class_with_type_hints():
33
35
  expected = {
34
36
  "type": "record",
35
37
  "name": "PyType",
38
+ "namespace": "test_plain_class",
39
+ "aliases": ["test_plain_class.OldPyType"],
36
40
  "fields": [
37
41
  {
38
42
  "name": "name",
@@ -54,7 +58,7 @@ def test_plain_class_with_type_hints():
54
58
  ],
55
59
  }
56
60
 
57
- assert_schema(PyType, expected)
61
+ assert_schema(PyType, expected, do_auto_namespace=True)
58
62
 
59
63
 
60
64
  def test_plain_class_annotated():
@@ -78,6 +82,25 @@ def test_plain_class_annotated():
78
82
  assert_schema(Annotated[PyType, ...], expected)
79
83
 
80
84
 
85
+ def test_field_alias():
86
+ class PyType:
87
+ def __init__(self, name: Annotated[str, Alias("old_name")]):
88
+ self.name = name
89
+
90
+ expected = {
91
+ "type": "record",
92
+ "name": "PyType",
93
+ "fields": [
94
+ {
95
+ "aliases": ["old_name"],
96
+ "name": "name",
97
+ "type": "string",
98
+ },
99
+ ],
100
+ }
101
+ assert_schema(PyType, expected)
102
+
103
+
81
104
  def test_plain_class_no_type_hints():
82
105
  class PyType:
83
106
  """A port, not using dataclass"""
@@ -17,6 +17,7 @@ import pydantic
17
17
  import pytest
18
18
 
19
19
  import py_avro_schema as pas
20
+ from py_avro_schema._alias import Alias
20
21
  from py_avro_schema._testing import assert_schema
21
22
 
22
23
 
@@ -496,6 +497,24 @@ def test_annotated_decimal():
496
497
  assert_schema(PyType, expected)
497
498
 
498
499
 
500
+ def test_field_alias():
501
+ class PyType(pydantic.BaseModel):
502
+ field_a: Annotated[str, Alias("field_b")]
503
+
504
+ expected = {
505
+ "type": "record",
506
+ "name": "PyType",
507
+ "fields": [
508
+ {
509
+ "aliases": ["field_b"],
510
+ "name": "field_a",
511
+ "type": "string",
512
+ }
513
+ ],
514
+ }
515
+ assert_schema(PyType, expected)
516
+
517
+
499
518
  def test_annotated_decimal_in_base():
500
519
  class Base(pydantic.BaseModel):
501
520
  field_a: Annotated[
@@ -1,5 +1,6 @@
1
- from typing import TypedDict
1
+ from typing import Annotated, TypedDict
2
2
 
3
+ from py_avro_schema._alias import Alias, register_type_alias
3
4
  from py_avro_schema._testing import assert_schema
4
5
 
5
6
 
@@ -27,8 +28,9 @@ def test_typed_dict():
27
28
 
28
29
 
29
30
  def test_type_dict_nested():
31
+ @register_type_alias("test_typed_dict.OldAddress")
30
32
  class Address(TypedDict):
31
- street: str
33
+ street: Annotated[str, Alias("address")]
32
34
  number: int
33
35
 
34
36
  class User(TypedDict):
@@ -39,6 +41,7 @@ def test_type_dict_nested():
39
41
  expected = {
40
42
  "type": "record",
41
43
  "name": "User",
44
+ "namespace": "test_typed_dict",
42
45
  "fields": [
43
46
  {
44
47
  "name": "name",
@@ -49,13 +52,36 @@ def test_type_dict_nested():
49
52
  "name": "address",
50
53
  "type": {
51
54
  "name": "Address",
55
+ "namespace": "test_typed_dict",
56
+ "aliases": ["test_typed_dict.OldAddress"],
52
57
  "type": "record",
53
58
  "fields": [
54
- {"name": "street", "type": "string"},
59
+ {"aliases": ["address"], "name": "street", "type": "string"},
55
60
  {"name": "number", "type": "long"},
56
61
  ],
57
62
  },
58
63
  },
59
64
  ],
60
65
  }
66
+ assert_schema(User, expected, do_auto_namespace=True)
67
+
68
+
69
+ def test_field_alias():
70
+ class User(TypedDict):
71
+ name: Annotated[str, Alias("username")]
72
+ age: int
73
+
74
+ expected = {
75
+ "type": "record",
76
+ "name": "User",
77
+ "fields": [
78
+ {
79
+ "aliases": ["username"],
80
+ "name": "name",
81
+ "type": "string",
82
+ },
83
+ {"name": "age", "type": "long"},
84
+ ],
85
+ }
86
+
61
87
  assert_schema(User, expected)
@@ -14,8 +14,6 @@
14
14
 
15
15
  [tox]
16
16
  envlist =
17
- py39
18
- py310
19
17
  py311
20
18
  py312
21
19
  py313
@@ -28,8 +26,6 @@ skip_missing_interpreters = true
28
26
  [gh-actions]
29
27
  # Mapping from GitHub Actions Python versions to Tox environments
30
28
  python =
31
- 3.9: py39
32
- 3.10: py310
33
29
  3.11: py311
34
30
  3.12: py312
35
31
  3.13: py313, linting, docs