Repository: marcosschroh/dataclasses-avroschema

Issue:
Problem with persistent schema and model inheritance
Thanks for this great package! I use it a lot for registering Faust Records in a schema registry. A problem that I noticed is that the Avro schemes are not correct when complex models are built which inherit from other models.

**Describe the bug**
When a model is subclassed from another model, the generated schema is persisted from the parent model. This can lead to some strange behavior:
 
**To Reproduce**
```python
@dataclasses.dataclass
class Common(AvroModel):
    some_data: str

@dataclasses.dataclass
class DerivedA(Common):
    some_more_data_A: str

@dataclasses.dataclass
class DerivedB(Common):
    some_more_data_B: str

print(Common.avro_schema())
{"type": "record", "name": "Common", "fields": [{"name": "some_data", "type": "string"}], "doc": "Common(some_data: str)"}
print(DerivedA.avro_schema())
{"type": "record", "name": "Common", "fields": [{"name": "some_data", "type": "string"}], "doc": "Common(some_data: str)"}
print(DerivedB.avro_schema())
{"type": "record", "name": "Common", "fields": [{"name": "some_data", "type": "string"}], "doc": "Common(some_data: str)"}
```
It is different, if the schema for Common is not generated:
```python
#print(Common.avro_schema())
print(DerivedA.avro_schema())
{"type": "record", "name": "DerivedA", "fields": [{"name": "some_data", "type": "string"}, {"name": "some_more_data_A", "type": "string"}], "doc": "DerivedA(some_data: str, some_more_data_A: str)"}
print(DerivedB.avro_schema())
{"type": "record", "name": "DerivedB", "fields": [{"name": "some_data", "type": "string"}, {"name": "some_more_data_B", "type": "string"}], "doc": "DerivedB(some_data: str, some_more_data_B: str)"}
```
This is the result that is expected
**Expected behavior**
See above. I believe the problem occurs due to this line here in ``schema_generator.py``:
```python
    @classmethod
    def generate_schema(cls: Type[CT], schema_type: str = "avro") -> Optional[OrderedDict]:
        if cls.schema_def is None: # <-- schema is persisted
```
I don't know what would be the pythonic way to approach this. A simple quick fix when using this is simply to set the schema of the base class to None before generating the dependent schemes:
```python
print(Common.avro_schema())
{"type": "record", "name": "Common", "fields": [{"name": "some_data", "type": "string"}], "doc": "Common(some_data: str)"}
Common.schema_def=None
print(DerivedA.avro_schema())
{"type": "record", "name": "DerivedA", "fields": [{"name": "some_data", "type": "string"}, {"name": "some_more_data_A", "type": "string"}], "doc": "DerivedA(some_data: str, some_more_data_A: str)"}
Common.schema_def=None
print(DerivedB.avro_schema())
{"type": "record", "name": "DerivedB", "fields": [{"name": "some_data", "type": "string"}, {"name": "some_more_data_B", "type": "string"}], "doc": "DerivedB(some_data: str, some_more_data_B: str)"}
``` 

Discussion:
github-actions[bot]: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.2.0
:---:
:rocket: Deployed preview to https://marcosschroh.github.io/dataclasses-avroschema/pr-preview/pr-243/
on branch [`gh-pages`](https://github.com/marcosschroh/dataclasses-avroschema/tree/gh-pages) at 2023-02-14 15:01 UTC
<!-- Sticky Pull Request Commentpr-preview -->
codecov[bot]: # [Codecov](https://codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/243?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh) Report
> Merging [#243](https://codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/243?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh) (bede186) into [master](https://codecov.io/gh/marcosschroh/dataclasses-avroschema/commit/2a100666c93afb3f4916ea84b2ed9904b71a3632?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh) (2a10066) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master     #243   +/-   ##
=======================================
  Coverage   99.37%   99.37%           
=======================================
  Files          14       14           
  Lines        1281     1281           
  Branches      264      264           
=======================================
  Hits         1273     1273           
  Misses          3        3           
  Partials        5        5           
```


| [Impacted Files](https://codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/243?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh) | Coverage Δ | |
|---|---|---|
| [dataclasses\_avroschema/schema\_definition.py](https://codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/243?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh#diff-ZGF0YWNsYXNzZXNfYXZyb3NjaGVtYS9zY2hlbWFfZGVmaW5pdGlvbi5weQ==) | `98.57% <ø> (ø)` | |
| [dataclasses\_avroschema/fields.py](https://codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/243?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh#diff-ZGF0YWNsYXNzZXNfYXZyb3NjaGVtYS9maWVsZHMucHk=) | `99.37% <100.00%> (ø)` | |
| [dataclasses\_avroschema/schema\_generator.py](https://codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/243?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh#diff-ZGF0YWNsYXNzZXNfYXZyb3NjaGVtYS9zY2hlbWFfZ2VuZXJhdG9yLnB5) | `100.00% <100.00%> (ø)` | |
| [dataclasses\_avroschema/serialization.py](https://codecov.io/gh/marcosschroh/dataclasses-avroschema/pull/243?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh#diff-ZGF0YWNsYXNzZXNfYXZyb3NjaGVtYS9zZXJpYWxpemF0aW9uLnB5) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Marcos+Schroh)


Patch:
diff --git a/dataclasses_avroschema/fields.py b/dataclasses_avroschema/fields.py
index 4e2aac5..c263d29 100644
--- a/dataclasses_avroschema/fields.py
+++ b/dataclasses_avroschema/fields.py
@@ -38,7 +38,6 @@ p = inflect.engine()
 
 @dataclasses.dataclass  # type: ignore
 class BaseField:
-
     __slots__ = (
         "name",
         "type",
@@ -489,7 +488,6 @@ class EnumField(BaseField):
             return f"{namespace}.{name}"
 
     def get_default_value(self) -> typing.Union[str, dataclasses._MISSING_TYPE, None]:
-
         if self.default == types.MissingSentinel:
             return dataclasses.MISSING
         elif self.default in (dataclasses.MISSING, None):
@@ -786,7 +784,6 @@ class RecordField(BaseField):
 
 @dataclasses.dataclass
 class DecimalField(BaseField):
-
     precision: int = -1
     scale: int = 0
 
@@ -796,8 +793,8 @@ class DecimalField(BaseField):
     def set_precision_scale(self) -> None:
         if self.default != types.MissingSentinel:
             if isinstance(self.default, decimal.Decimal):
-                sign, digits, scale = self.default.as_tuple()
-                self.scale = scale * -1  # Make scale positive, as that's what Avro expects
+                _, digits, scale = self.default.as_tuple()
+                self.scale = int(scale) * -1  # Make scale positive, as that's what Avro expects
                 # decimal.Context has a precision property
                 # BUT the precision property is independent of the number of digits stored in the Decimal instance
                 # # # FROM THE DOCS HERE https://docs.python.org/3/library/decimal.html
diff --git a/dataclasses_avroschema/schema_definition.py b/dataclasses_avroschema/schema_definition.py
index da419e4..103cf65 100644
--- a/dataclasses_avroschema/schema_definition.py
+++ b/dataclasses_avroschema/schema_definition.py
@@ -49,7 +49,6 @@ class BaseSchemaDefinition(abc.ABC):
 
 @dataclasses.dataclass
 class AvroSchemaDefinition(BaseSchemaDefinition):
-
     fields: typing.List[FieldType] = dataclasses.field(default_factory=list)
 
     def __post_init__(self) -> None:
diff --git a/dataclasses_avroschema/schema_generator.py b/dataclasses_avroschema/schema_generator.py
index 8ede7d7..36a746c 100644
--- a/dataclasses_avroschema/schema_generator.py
+++ b/dataclasses_avroschema/schema_generator.py
@@ -44,7 +44,7 @@ class AvroModel:
 
     @classmethod
     def generate_schema(cls: Type[CT], schema_type: str = "avro") -> Optional[OrderedDict]:
-        if cls.schema_def is None:
+        if cls.schema_def is None or cls.__mro__[1] != AvroModel:
             # Generate dataclass and metadata
             cls.klass = cls.generate_dataclass()
 
@@ -170,7 +170,6 @@ class AvroModel:
         create_instance: bool = True,
         writer_schema: Optional[Union[JsonDict, Type[CT]]] = None,
     ) -> Union[JsonDict, CT]:
-
         if inspect.isclass(writer_schema) and issubclass(writer_schema, AvroModel):
             # mypy does not undersdtand redefinitions
             writer_schema: JsonDict = writer_schema.avro_schema_to_python()  # type: ignore
diff --git a/dataclasses_avroschema/serialization.py b/dataclasses_avroschema/serialization.py
index a5c3c81..ae32030 100644
--- a/dataclasses_avroschema/serialization.py
+++ b/dataclasses_avroschema/serialization.py
@@ -106,7 +106,7 @@ def prepare_bytes_decimal(data: decimal.Decimal, precision: int, scale: int = 0)
     if len(digits) > precision:
         raise ValueError("The decimal precision is bigger than allowed by schema")
 
-    delta = exp + scale
+    delta = int(exp) + scale
 
     if delta < 0:
         raise ValueError("Scale provided in schema does not match the decimal")


Test patch:
diff --git a/tests/schemas/test_schema.py b/tests/schemas/test_schema.py
index 05f63be..bde9754 100644
--- a/tests/schemas/test_schema.py
+++ b/tests/schemas/test_schema.py
@@ -303,7 +303,7 @@ def test_parse_obj():
     assert User.avro_schema()
 
 
-def test_inheritance(user_avro_json: JsonDict) -> None:
+def test_avro_schema_to_python_method_with_inheritance(user_avro_json: JsonDict) -> None:
     @dataclass
     class Parent(AvroModel):
         name: str
@@ -325,3 +325,34 @@ def test_inheritance(user_avro_json: JsonDict) -> None:
     child_2_schema = Child2.avro_schema_to_python()
 
     assert child_schema["fields"] == child_2_schema["fields"] == user_avro_json["fields"]
+
+
+def test_avro_schema_method_with_inheritance() -> None:
+    @dataclass
+    class Common(AvroModel):
+        some_data: str
+
+    @dataclass
+    class DerivedA(Common):
+        some_more_data_A: str
+
+    @dataclass
+    class DerivedB(Common):
+        some_more_data_B: str
+
+    common_schema = Common.avro_schema()
+    derived_a_schema = DerivedA.avro_schema()
+    derived_b_schema = DerivedB.avro_schema()
+
+    assert (
+        common_schema
+        == '{"type": "record", "name": "Common", "fields": [{"name": "some_data", "type": "string"}], "doc": "Common(some_data: str)"}'
+    )
+    assert (
+        derived_a_schema
+        == '{"type": "record", "name": "DerivedA", "fields": [{"name": "some_data", "type": "string"}, {"name": "some_more_data_A", "type": "string"}], "doc": "DerivedA(some_data: str, some_more_data_A: str)"}'
+    )
+    assert (
+        derived_b_schema
+        == '{"type": "record", "name": "DerivedB", "fields": [{"name": "some_data", "type": "string"}, {"name": "some_more_data_B", "type": "string"}], "doc": "DerivedB(some_data: str, some_more_data_B: str)"}'
+    )