Skip to content

Commit

Permalink
fix: generate proper Model when there is name clashe between field na…
Browse files Browse the repository at this point in the history
…me and type hints. Closes #769 (#778)
  • Loading branch information
marcosschroh authored Oct 21, 2024
1 parent 46b6a67 commit cbe0950
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 91 deletions.
180 changes: 89 additions & 91 deletions dataclasses_avroschema/model_generator/lang/python/base.py

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions docs/schema_relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
135 changes: 135 additions & 0 deletions tests/model_generator/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
65 changes: 65 additions & 0 deletions tests/model_generator/test_model_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
35 changes: 35 additions & 0 deletions tests/schemas/avro/clashes_types_schema.avsc
Original file line number Diff line number Diff line change
@@ -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
}
]
}
5 changes: 5 additions & 0 deletions tests/schemas/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading

0 comments on commit cbe0950

Please sign in to comment.