Skip to content

Commit

Permalink
fix: generate proper python identifiers when generating model from sc…
Browse files Browse the repository at this point in the history
…hemas. Related to #628 (#639)
  • Loading branch information
marcosschroh committed May 24, 2024
1 parent f7d032e commit 8e29eee
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 3 deletions.
8 changes: 7 additions & 1 deletion dataclasses_avroschema/model_generator/lang/python/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def render_field(self, field: JsonDict, model_name: str) -> FieldRepresentation:
3. If the field is a LogicalType, it may not have the
the `name` property and the type is a `native` one
"""
name = field.get("name", "")
name = self.generate_field_name(field.get("name", ""))
type: AvroTypeRepr = field["type"]
default = field.get("default", dataclasses.MISSING)
field_metadata = self.get_field_metadata(field)
Expand Down Expand Up @@ -255,6 +255,12 @@ def render_field(self, field: JsonDict, model_name: str) -> FieldRepresentation:

return FieldRepresentation(name=name, string_representation=result, has_default=has_default)

@staticmethod
def generate_field_name(field_name: str) -> str:
if field_name and not field_name.isidentifier():
return casefy.snakecase(field_name)
return field_name

@staticmethod
def is_logical_type(*, field: JsonDict) -> bool:
if field.get("logicalType"):
Expand Down
43 changes: 42 additions & 1 deletion docs/model_generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,48 @@ print(User.fake())
# >>> User(name='JBZdhEWdXwFLQitWCjkc', age=3406, address=Address(name='AhlQsvXnkpcPZJvRSXLr'))
```

## Schema with invalid python identifiers

`avro schemas` could contain field names that are not valid `python identifiers`, for example `street-name`. If we have the following `avro schema` the `python model` generated from it will generate `valid identifiers`, in this case and `street_name` and `street_number`

```python
from dataclasses_avroschema import ModelGenerator


schema = {
"type": "record",
"name": "Address",
"fields": [
{"name": "street-name", "type": "string"},
{"name": "street-number", "type": "long"}
]
}

model_generator = ModelGenerator()
result = model_generator.render(schema=schema)

# save the result in a file
with open("models.py", mode="+w") as f:
f.write(result)
```

Then the result will be:

```python
# models.py
from dataclasses_avroschema import AvroModel
import dataclasses


@dataclasses.dataclass
class Address(AvroModel):
street_name: str
street_number: int
```

!!! warning
If you try to generate the `schema` from the model, both schemas won't match. You might have to use the [case](https://marcosschroh.github.io/dataclasses-avroschema/case/) functionality

## Field order

Sometimes we have to work with schemas that were created by a third party and we do not have control over them. Those schemas can contain optional fields
Expand Down Expand Up @@ -462,7 +504,6 @@ class UnitMultiPlayer(enum.Enum):
@dataclasses.dataclass
class User(AvroModel):
unit_multi_player: UnitMultiPlayer

```

As the example shows the second enum member `UnitMultiPlayer.p` is not in uppercase otherwise will collide with the first member `UnitMultiPlayer.P`
Expand Down
2 changes: 1 addition & 1 deletion scripts/test
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ fi
tests=${1-"./tests"}

${PREFIX}pytest --cov=dataclasses_avroschema ${tests} ${2} --cov-fail-under=99 --cov-report html --cov-report term-missing --cov-report xml
${PREFIX}ruff dataclasses_avroschema tests
${PREFIX}ruff check dataclasses_avroschema tests
${PREFIX}mypy dataclasses_avroschema
16 changes: 16 additions & 0 deletions tests/model_generator/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ def schema_2() -> Dict:
}


@pytest.fixture
def schema_with_invalid_python_identifiers() -> Dict:
return {
"type": "record",
"name": "Address",
"fields": [
{"name": "street-name", "type": "string"},
{"name": "street-number", "type": "long"},
{"name": "ValidIdentifier", "type": "string"},
{"name": "anotherIdentifier", "type": "string"},
{"name": "_private", "type": "string"},
],
"doc": "An Address",
}


@pytest.fixture
def schema_primitive_types_as_defined_types() -> Dict:
return {
Expand Down
24 changes: 24 additions & 0 deletions tests/model_generator/test_model_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ class Meta:
assert result.strip() == expected_result.strip()


def test_schema_with_invalid_python_identifiers(schema_with_invalid_python_identifiers: types.JsonDict) -> None:
expected_result = """
from dataclasses_avroschema import AvroModel
import dataclasses
@dataclasses.dataclass
class Address(AvroModel):
\"""
An Address
\"""
street_name: str
street_number: int
ValidIdentifier: str
anotherIdentifier: str
_private: str
"""
model_generator = ModelGenerator()
result = model_generator.render(schema=schema_with_invalid_python_identifiers)
assert result.strip() == expected_result.strip()


def test_model_generator_primitive_types_as_defined_types(
schema_primitive_types_as_defined_types: types.JsonDict,
) -> None:
Expand Down

0 comments on commit 8e29eee

Please sign in to comment.