From cbe0950dbaa6ccf38ced39fb4f3aa951611566b8 Mon Sep 17 00:00:00 2001 From: Marcos Schroh <2828842+marcosschroh@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:58:55 +0200 Subject: [PATCH] fix: generate proper Model when there is name clashe between field name and type hints. Closes #769 (#778) --- .../model_generator/lang/python/base.py | 180 +++++++++--------- docs/schema_relationships.md | 78 ++++++++ tests/model_generator/conftest.py | 135 +++++++++++++ tests/model_generator/test_model_generator.py | 65 +++++++ tests/schemas/avro/clashes_types_schema.avsc | 35 ++++ tests/schemas/conftest.py | 5 + tests/schemas/test_nested_schemas.py | 15 ++ 7 files changed, 422 insertions(+), 91 deletions(-) create mode 100644 tests/schemas/avro/clashes_types_schema.avsc diff --git a/dataclasses_avroschema/model_generator/lang/python/base.py b/dataclasses_avroschema/model_generator/lang/python/base.py index 7b33e2c0..b86bf46d 100644 --- a/dataclasses_avroschema/model_generator/lang/python/base.py +++ b/dataclasses_avroschema/model_generator/lang/python/base.py @@ -25,15 +25,12 @@ class FieldRepresentation: default: typing.Any template: Template metadata: JsonDict = dataclasses.field(default_factory=dict) - is_complex_type: bool = False children: typing.List["FieldRepresentation"] = dataclasses.field(default_factory=list) inner_name: str = "" def __post_init__(self) -> None: if isinstance(self.avro_type, list): self.avro_type = self.avro_type[0] - for child in self.children: - self.metadata.update(child.metadata) if isinstance(self.avro_type, dict): inner_type = self.avro_type["type"] @@ -44,7 +41,6 @@ def __post_init__(self) -> None: self.avro_type = self.child_field.avro_type self.type_hint = self.type_hint or self.child_field.type_hint self.default = self.default if self.default is not dataclasses.MISSING else self.child_field.default - self.metadata.update(self.child_field.metadata) # Check that internal field name is different to the original name # for example: {"name": "age", "type": { "type": "array", "items": "string", "name": "my_age" }} @@ -56,17 +52,15 @@ def __post_init__(self) -> None: ): self.metadata["inner_name"] = self.inner_name + # update all metadata based on all children + for child in self.children: + self.metadata.update(child.metadata) + def to_string(self) -> str: """ Convert a field to string """ - if self.is_complex_type or not self.name: - # If the field is a complext type or - # name is an empty string (it means that the type is a native type - # with the form {"type": "a_primitive_type"}, example {"type": "string"}) - result = self.type_hint - else: - result = templates.field_type_template.safe_substitute(name=self.name, type=self.type_hint) + result = templates.field_type_template.safe_substitute(name=self.name, type=self.type_hint) # optional field attribute default_generated = self.get_field_default() @@ -215,7 +209,7 @@ def render(self, schemas: typing.List[JsonDict]) -> str: result.append(self.render_class(schema=schema)) else: # If is not a Record then it is an Enum - self.render_field(field=schema, model_name="") + self.render_field(field=schema, model_name="", parent_field_name=schema["name"]) classes = "\n".join(result) imports = self.render_imports() @@ -301,17 +295,19 @@ def render_docstring(self, *, docstring: typing.Optional[str]) -> str: return f'{self.field_identation}"""{indented}{self.field_identation}"""' - def render_class(self, *, schema: JsonDict) -> str: + def render_class(self, *, schema: JsonDict, parent_field_name: typing.Optional[str] = None) -> str: """ Render the class generated from the schema """ - name: str = casefy.pascalcase(schema["name"]) + model_name: str = casefy.pascalcase(schema["name"]) + record_fields: typing.List[JsonDict] = schema["fields"] self.add_class_imports() # Sort the fields according whether it has a default value fields_representation: typing.List[FieldRepresentation] = [ - self.render_field(field=field, model_name=name) for field in record_fields + self.render_field(field=field, model_name=model_name, parent_field_name=parent_field_name or field["name"]) + for field in record_fields ] fields_representation_copy = copy.copy(fields_representation) @@ -325,21 +321,21 @@ def render_class(self, *, schema: JsonDict) -> str: docstring = self.render_docstring(docstring=schema.get("doc")) rendered_class = templates.class_template.safe_substitute( - name=name, + name=model_name, decorator=self.base_class_decorator, base_class=self.base_class, fields=rendered_fields_string, docstring=docstring, ) - add_schema_name = name != schema["name"] + add_schema_name = model_name != schema["name"] class_metadata = self.render_metaclass(schema=schema, field_order=field_order, add_schema_name=add_schema_name) if class_metadata is not None: rendered_class += class_metadata return rendered_class - def render_field(self, field: JsonDict, model_name: str) -> FieldRepresentation: + def render_field(self, field: JsonDict, model_name: str, parent_field_name: str) -> FieldRepresentation: """ Render an avro field. @@ -351,48 +347,47 @@ def render_field(self, field: JsonDict, model_name: str) -> FieldRepresentation: """ name = self.generate_field_name(field) - type: AvroTypeRepr = field["type"] + avro_type: AvroTypeRepr = field["type"] default = field.get("default", dataclasses.MISSING) field_metadata = self.get_field_metadata(field) children = None - # This flag tells whether the field is array, map, fixed - is_complex_type = False - if self.is_logical_type(field=field): - is_complex_type = True # override the type so it can be use to get the default value in case that is needed - type = field.get("logicalType") or field["type"]["logicalType"] + avro_type = field.get("logicalType") or field["type"]["logicalType"] type_hint = self.parse_logical_type(field=field) - elif isinstance(type, dict): + elif isinstance(avro_type, dict): type_hint = "" - children = [self.render_field(field=type, model_name=model_name)] - elif isinstance(type, list): - type_hint, children = self.parse_union(field_types=type, model_name=model_name) - elif type == field_utils.ARRAY: - is_complex_type = True - type_hint = self.parse_array(field=field, model_name=model_name) - elif type == field_utils.MAP: - is_complex_type = True - type_hint = self.parse_map(field=field, model_name=model_name) - elif type == field_utils.ENUM: - is_complex_type = True + children = [self.render_field(field=avro_type, model_name=model_name, parent_field_name=name)] + elif isinstance(avro_type, list): + type_hint, children = self.parse_union(field_types=avro_type, model_name=model_name, parent_field_name=name) + elif avro_type == field_utils.ARRAY: + type_hint, children = self.parse_array(field=field, model_name=model_name, parent_field_name=name) + elif avro_type == field_utils.MAP: + type_hint, children = self.parse_map(field=field, model_name=model_name, parent_field_name=name) + elif avro_type == field_utils.ENUM: type_hint = self.parse_enum(field=field) # We must set the default Enums type level default to dataclasses.MISSING # as it is set in the Meta class. # Check https://github.com/marcosschroh/dataclasses-avroschema/issues/665 default = dataclasses.MISSING - elif type == field_utils.FIXED: - is_complex_type = True + elif avro_type == field_utils.FIXED: type_hint = self.parse_fixed(field=field) - elif type == field_utils.RECORD: - record = f"\n{self.render_class(schema=field)}" - is_complex_type = True + elif avro_type == field_utils.RECORD: + type_hint = casefy.pascalcase(name) + + record = f"\n{self.render_class(schema=field, parent_field_name=name)}" self.extras.append(record) - type_hint = casefy.pascalcase(field["name"]) + + if parent_field_name == type_hint: + type_hint = f"_{name}" + clashed_class = ( + f"\n{templates.metaclass_alias_field_template.safe_substitute(name=type_hint, value=name)}" + ) + self.extras.append(clashed_class) else: # Native field - type_hint = self.get_language_type(type=type, model_name=model_name) + type_hint = self.get_language_type(avro_type=avro_type, field_name=name, model_name=model_name) # check if the language type must be replaced with some extra class # specified in the field metadata only after the native type was resolved @@ -401,10 +396,9 @@ def render_field(self, field: JsonDict, model_name: str) -> FieldRepresentation: return FieldRepresentation( name=name, - avro_type=type, + avro_type=avro_type, type_hint=type_hint, template=self.field_template, - is_complex_type=is_complex_type, default=default, metadata=field_metadata, children=children or [], @@ -443,23 +437,19 @@ def parse_logical_type(self, *, field: JsonDict) -> str: type_from_metadata = self._resolve_type_from_metadata(field=field) if type_from_metadata is not None: - type = type_from_metadata + type_hint = type_from_metadata elif logical_type == field_utils.DECIMAL: # this is a special case for logical types - type = self.parse_decimal(field=field, default=default) + type_hint = self.parse_decimal(field=field, default=default) elif logical_type not in self.logical_types_imports: # Then it is a custom logicalType, so we default to the native type - type = self.get_language_type(type=field["type"]) + type_hint = self.get_language_type(avro_type=field["type"]) else: # add the logical type import self.imports.add(self.logical_types_imports[logical_type]) - type = self.avro_type_to_lang[logical_type] + type_hint = self.avro_type_to_lang[logical_type] - if field_name is not None: - field_repr = templates.field_type_template.safe_substitute(name=field_name, type=type) - return field_repr - - return type + return type_hint def parse_decimal(self, *, field: JsonDict, default: typing.Optional[str] = None) -> str: precision = field["precision"] @@ -477,7 +467,7 @@ def parse_decimal(self, *, field: JsonDict, default: typing.Optional[str] = None return field_repr def parse_union( - self, *, field_types: typing.List, model_name: str + self, *, field_types: typing.List, model_name: str, parent_field_name: str ) -> typing.Tuple[str, typing.List[FieldRepresentation]]: """ Parse an Avro union @@ -494,10 +484,9 @@ def parse_union( # XXX: Maybe more useful in general def render_type(avro_type: typing.Union[str, dict]) -> FieldRepresentation: - if isinstance(avro_type, str): - avro_type = {"type": avro_type} + avro_type = self.type_to_defined_types(avro_type) - return self.render_field(field=avro_type, model_name=model_name) + return self.render_field(field=avro_type, model_name=model_name, parent_field_name=parent_field_name) if field_utils.NULL in field_types and len(field_types) == 2: # It is an optional field, we should include in the imports typing @@ -507,7 +496,6 @@ def render_type(avro_type: typing.Union[str, dict]) -> FieldRepresentation: field_representation = render_type(field_type[0]) type_hint = templates.optional_template.safe_substitute(type=field_representation.type_hint) return type_hint, [field_representation] - # return templates.optional_template.safe_substitute(type=language_types) elif len(field_types) >= 2: # a union with more than 2 types self.imports.add("import typing") @@ -521,7 +509,9 @@ def render_type(avro_type: typing.Union[str, dict]) -> FieldRepresentation: field_representation = render_type(field_types[0]) return field_representation.type_hint, [field_representation] - def parse_array(self, field: JsonDict, model_name: str) -> str: + def parse_array( + self, field: JsonDict, model_name: str, parent_field_name: str + ) -> typing.Tuple[str, typing.List[FieldRepresentation]]: """ Parse an Avro array @@ -530,12 +520,19 @@ def parse_array(self, field: JsonDict, model_name: str) -> str: Example: {"name": "pets", "type": {"type": "array", "items": "string", "name": "pet"}} """ - type = field["items"] - language_type = self._get_complex_langauge_type(type=type, model_name=model_name) + self.imports.add("import typing") + avro_type = self.type_to_defined_types(field["items"]) + + field_representation = self.render_field( + field=avro_type, model_name=model_name, parent_field_name=parent_field_name + ) + type_hint = templates.list_template.safe_substitute(type=field_representation.type_hint) - return templates.list_template.safe_substitute(type=language_type) + return type_hint, [field_representation] - def parse_map(self, field: JsonDict, model_name: str) -> str: + def parse_map( + self, field: JsonDict, model_name: str, parent_field_name: str + ) -> typing.Tuple[str, typing.List[FieldRepresentation]]: """ Parse an Avro map @@ -544,10 +541,15 @@ def parse_map(self, field: JsonDict, model_name: str) -> str: Example: {"name": "accounts_money", "type": {"type": "map", "values": "float", "name": "accounts_money"}}, """ - type = field["values"] - language_type = self._get_complex_langauge_type(type=type, model_name=model_name) + self.imports.add("import typing") + avro_type = self.type_to_defined_types(field["values"]) + + field_representation = self.render_field( + field=avro_type, model_name=model_name, parent_field_name=parent_field_name + ) + type_hint = templates.dict_template.safe_substitute(type=field_representation.type_hint) - return templates.dict_template.safe_substitute(type=language_type) + return type_hint, [field_representation] def parse_fixed(self, field: JsonDict) -> str: self.imports.add("from dataclasses_avroschema import types") @@ -605,44 +607,40 @@ def parse_enum(self, field: JsonDict) -> str: return enum_name - def _get_complex_langauge_type(self, *, type: typing.Any, model_name: str) -> str: - """ - Get the language type for complex types (array and maps) - """ - self.imports.add("import typing") - - if isinstance(type, dict): - field_representation = self.render_field(field=type, model_name=model_name) - language_type = field_representation.to_string() - elif isinstance(type, list): - language_type, _ = self.parse_union(field_types=type, model_name=model_name) - else: - language_type = self.get_language_type(type=type, model_name=model_name) - - return language_type - def get_language_type( self, *, - type: str, + avro_type: str, + field_name: typing.Optional[str] = None, default: typing.Optional[str] = None, model_name: typing.Optional[str] = None, ) -> str: - if type in (field_utils.INT, field_utils.FLOAT): + if avro_type in (field_utils.INT, field_utils.FLOAT): self.imports.add("from dataclasses_avroschema import types") - if type == model_name: + if avro_type == model_name: # it means that it is a one-to-self-relationship - return templates.type_template.safe_substitute(type=type) - elif type not in self.avro_type_to_lang: + return templates.type_template.safe_substitute(type=avro_type) + elif avro_type not in self.avro_type_to_lang: # it means the type points to the an already specified type so it contains the type name # with optional namespaces, e.g. my_namespace.users.User # In this case we should return the last part of the string - return casefy.pascalcase(type.split(".")[-1]) + type_hint = casefy.pascalcase(avro_type.split(".")[-1]) + + if field_name == type_hint: + type_hint = f"_{avro_type}" + return type_hint elif default is not None: - return str(self.avro_type_to_lang.get(type, default)) + return str(self.avro_type_to_lang.get(avro_type, default)) else: - return str(self.avro_type_to_lang.get(type, type)) + return str(self.avro_type_to_lang.get(avro_type, avro_type)) + + @staticmethod + def type_to_defined_types(avro_type: typing.Any) -> JsonDict: + if isinstance(avro_type, (str, list)): + avro_type = {"type": avro_type} + + return avro_type def get_field_metadata(self, field: JsonDict) -> JsonDict: keys_to_ignore = [ diff --git a/docs/schema_relationships.md b/docs/schema_relationships.md index 5da58961..e538ecef 100644 --- a/docs/schema_relationships.md +++ b/docs/schema_relationships.md @@ -539,3 +539,81 @@ class User(AvroModel): ``` *(This script is complete, it should run "as is")* + +## Naming clashes + +Sometimes theare are `avro schemas` that contain name clashing between `field names` and `type names`, for example the following schema the record `Message` has a field called `MessageHeader` which is also a `type` record: + +```json +{ + "type": "record", + "name": "Message", + "fields": [ + {"name": "MessageBody", "type": "string"}, + { + "name": "MessageHeader", + "type": [ + "null", + { + "type": "array", + "name": "MessageHeader", + "items": { + "type": "record", + "name": "MessageHeader", + "fields": [ + {"name": "version", "type": "string"}, + {"name": "MessageType", "type": "string"} + ] + } + } + ], + "default": null + } + ] +} +``` + +From the previous schema we could have a model which might cause unexpected results: + +```python +from dataclasses_avroschema import AvroModel +import dataclasses +import typing + + +@dataclasses.dataclass +class MessageHeader(AvroModel): + version: str + MessageType: str + + +@dataclasses.dataclass +class Message(AvroModel): + MessageBody: str + MessageHeader: typing.Optional[typing.List[MessageHeader]] = None +``` + +If you try to use the `dataclasses` module and inspect the fields of the class `Message` doing `dataclasses.fields(Message)` you will see that the `typing hint` for the field `MessageHeader` is `typing.Optional[typing.List[NoneType]]`, which is should not be. This problem is cause by the way that `Python finds references` and because *type annotations are evaluated after assignments*. + +To solve this problem `dataclasses-avroschema` introduces just before the name clashing a new type definition which is used to set the `type hint` when it is required. Then `type` that causes the problem is defined outside the `class scope`. + +```python +from dataclasses_avroschema import AvroModel +import dataclasses +import typing + + +@dataclasses.dataclass +class MessageHeader(AvroModel): + version: str + MessageType: str + +_MessageHeader = MessageHeader + +@dataclasses.dataclass +class Message(AvroModel): + MessageBody: str + MessageHeader: typing.Optional[typing.List[_MessageHeader]] = None +``` + +As a result the `typing hint` for the field `MessageHeader` becomes `typing.Optional[typing.List[__main__.MessageHeader]]`, which is the correct one. diff --git a/tests/model_generator/conftest.py b/tests/model_generator/conftest.py index bd005e74..7390810e 100644 --- a/tests/model_generator/conftest.py +++ b/tests/model_generator/conftest.py @@ -472,6 +472,111 @@ def schema_one_to_one_relationship() -> JsonDict: } +@pytest.fixture +def schema_one_to_many_relationship_clashes_types() -> JsonDict: + return { + "type": "record", + "name": "Message", + "fields": [ + {"name": "MessageBody", "type": "string"}, + { + "name": "MessageHeader", + "type": [ + "null", + { + "type": "array", + "name": "MessageHeader", + "items": { + "type": "record", + "name": "MessageHeader", + "fields": [ + {"name": "version", "type": "string"}, + {"name": "MessageType", "type": "string"}, + ], + }, + }, + ], + "default": None, + }, + ], + } + + +@pytest.fixture +def schema_one_to_many_relationship_multiple_clashes_types() -> JsonDict: + return { + "type": "record", + "name": "Message", + "fields": [ + {"name": "MessageBody", "type": "string"}, + { + "name": "MessageHeader", + "type": [ + "null", + { + "type": "array", + "name": "MessageHeader", + "items": { + "type": "record", + "name": "MessageHeader", + "fields": [ + {"name": "version", "type": "string"}, + {"name": "MessageType", "type": "string"}, + ], + }, + }, + ], + "default": None, + }, + { + "name": "MessageHeader2", + "type": "MessageHeader", + }, + { + "name": "SuperMessageHeader", + "type": { + "type": "record", + "name": "SuperMessageHeader", + "fields": [ + {"name": "name", "type": "string"}, + {"name": "MessageHeader", "type": "MessageHeader"}, + ], + }, + }, + ], + } + + +@pytest.fixture +def schema_one_to_many_relationship_union_with_clashes_types() -> JsonDict: + return { + "type": "record", + "name": "Message", + "fields": [ + {"name": "MessageBody", "type": "string"}, + { + "name": "MessageHeader", + "type": [ + "null", + { + "type": "array", + "name": "MessageHeader", + "items": { + "type": "record", + "name": "MessageHeader", + "fields": [ + {"name": "version", "type": "string"}, + {"name": "MessageType", "type": "string"}, + ], + }, + }, + ], + "default": None, + }, + ], + } + + @pytest.fixture def schema_one_to_many_array_relationship() -> JsonDict: return { @@ -515,6 +620,36 @@ def schema_one_to_many_array_relationship() -> JsonDict: } +@pytest.fixture +def schema_one_to_one_relationship_clashes_types() -> JsonDict: + return { + "type": "record", + "name": "Message", + "fields": [ + {"name": "MessageBody", "type": "string"}, + { + "name": "MessageHeader", + "type": [ + "null", + { + "type": "array", + "name": "MessageHeader", + "items": { + "type": "record", + "name": "MessageHeader", + "fields": [ + {"name": "version", "type": "string"}, + {"name": "MessageType", "type": "string"}, + ], + }, + }, + ], + "default": None, + }, + ], + } + + @pytest.fixture def schema_one_to_many_map_relationship() -> JsonDict: return { diff --git a/tests/model_generator/test_model_generator.py b/tests/model_generator/test_model_generator.py index 5ede772e..9211462f 100644 --- a/tests/model_generator/test_model_generator.py +++ b/tests/model_generator/test_model_generator.py @@ -469,6 +469,71 @@ class User(AvroModel): assert result.strip() == expected_result.strip() +def test_schema_one_to_many_relationship_clashes_types( + schema_one_to_many_relationship_clashes_types: types.JsonDict, +) -> None: + expected_result = """ +from dataclasses_avroschema import AvroModel +import dataclasses +import typing + + +@dataclasses.dataclass +class MessageHeader(AvroModel): + version: str + MessageType: str + +_MessageHeader = MessageHeader + +@dataclasses.dataclass +class Message(AvroModel): + MessageBody: str + MessageHeader: typing.Optional[typing.List[_MessageHeader]] = None +""" + model_generator = ModelGenerator() + result = model_generator.render(schema=schema_one_to_many_relationship_clashes_types) + assert result.strip() == expected_result.strip() + + +def test_schema_onto_many_relationship_multiple_clashes_types( + schema_one_to_many_relationship_multiple_clashes_types: types.JsonDict, +) -> None: + expected_result = """ +from dataclasses_avroschema import AvroModel +import dataclasses +import typing + + +@dataclasses.dataclass +class MessageHeader(AvroModel): + version: str + MessageType: str + +_MessageHeader = MessageHeader + +@dataclasses.dataclass +class SuperMessageHeader(AvroModel): + name: str + MessageHeader: _MessageHeader + +_SuperMessageHeader = SuperMessageHeader + +@dataclasses.dataclass +class Message(AvroModel): + MessageBody: str + MessageHeader2: MessageHeader + SuperMessageHeader: _SuperMessageHeader + MessageHeader: typing.Optional[typing.List[_MessageHeader]] = None + + + class Meta: + field_order = ['MessageBody', 'MessageHeader', 'MessageHeader2', 'SuperMessageHeader'] +""" + model_generator = ModelGenerator() + result = model_generator.render(schema=schema_one_to_many_relationship_multiple_clashes_types) + assert result.strip() == expected_result.strip() + + def test_schema_one_to_many_array_relationship( schema_one_to_many_array_relationship: types.JsonDict, ) -> None: diff --git a/tests/schemas/avro/clashes_types_schema.avsc b/tests/schemas/avro/clashes_types_schema.avsc new file mode 100644 index 00000000..17aaef5a --- /dev/null +++ b/tests/schemas/avro/clashes_types_schema.avsc @@ -0,0 +1,35 @@ +{ + "type": "record", + "name": "Message", + "fields": [ + { + "name": "MessageBody", + "type": "string" + }, + { + "name": "MessageHeader", + "type": [ + "null", + { + "type": "array", + "items": { + "type": "record", + "name": "MessageHeader", + "fields": [ + { + "name": "version", + "type": "string" + }, + { + "name": "MessageType", + "type": "string" + } + ] + }, + "name": "MessageHeader" + } + ], + "default": null + } + ] + } \ No newline at end of file diff --git a/tests/schemas/conftest.py b/tests/schemas/conftest.py index 2dbda4af..5e1bdacc 100644 --- a/tests/schemas/conftest.py +++ b/tests/schemas/conftest.py @@ -52,6 +52,11 @@ def user_one_address_schema(): return load_json("user_one_address.avsc") +@pytest.fixture +def clashes_types_schema(): + return load_json("clashes_types_schema.avsc") + + @pytest.fixture def user_one_address_alias_item(): return load_json("user_one_address_alias_item.avsc") diff --git a/tests/schemas/test_nested_schemas.py b/tests/schemas/test_nested_schemas.py index b944d0ed..c1264c95 100644 --- a/tests/schemas/test_nested_schemas.py +++ b/tests/schemas/test_nested_schemas.py @@ -1,3 +1,4 @@ +import dataclasses import json import typing @@ -9,12 +10,14 @@ def test_one_to_one_relationship(user_one_address_schema): Test schema relationship one-to-one """ + @dataclasses.dataclass class Address(AvroModel): "An Address" street: str street_number: int + @dataclasses.dataclass class User(AvroModel): "An User with Address" @@ -30,12 +33,14 @@ def test_one_to_many_relationship(user_many_address_schema): Test schema relationship one-to-many """ + @dataclasses.dataclass class Address(AvroModel): "An Address" street: str street_number: int + @dataclasses.dataclass class User(AvroModel): "User with multiple Address" @@ -51,12 +56,14 @@ def test_one_to_many_map_relationship(user_many_address_map_schema): Test schema relationship one-to-many using a map """ + @dataclasses.dataclass class Address(AvroModel): "An Address" street: str street_number: int + @dataclasses.dataclass class User(AvroModel): "User with multiple Address" @@ -74,12 +81,14 @@ def test_one_to_many_map_relationship_with_alias( Test schema relationship one-to-many using a map """ + @dataclasses.dataclass class Address(AvroModel): "An Address" street: str street_number: int + @dataclasses.dataclass class User(AvroModel): "User with multiple Address" @@ -98,12 +107,14 @@ def test_alias_nested_item(user_one_address_alias_item): Test schema relationship one-to-one """ + @dataclasses.dataclass class Address(AvroModel): "An Address" street: str street_number: int + @dataclasses.dataclass class User(AvroModel): "An User with Address" @@ -118,12 +129,14 @@ class Meta: def test_alias_nested_item_with_array(user_array_address_alias): + @dataclasses.dataclass class Address(AvroModel): "An Address" street: str street_number: int + @dataclasses.dataclass class User(AvroModel): "An User with Address" @@ -138,12 +151,14 @@ class Meta: def test_alias_nested_item_with_map(user_map_address_alias): + @dataclasses.dataclass class Address(AvroModel): "An Address" street: str street_number: int + @dataclasses.dataclass class User(AvroModel): "An User with Address"