yukinarit / pyserde

Yet another serialization library on top of dataclasses, inspired by serde-rs.
https://yukinarit.github.io/pyserde/guide/en
MIT License
711 stars 40 forks source link

dataclass InitVar plus explicit serde decorator breaks metadata and, at least, skip option #581

Closed morrison12 closed 4 weeks ago

morrison12 commented 1 month ago

Even with the workaround from issue #426 applied, it appears the use of InitVar in a dataclass that has explicitly been decorated with @serde, somehow interferes with serdes' use of metadata: at least the skip= option is not recorded and thus the marked field appears in the output.

In the following example one would expect to see the relevant metadata and correct output in all four cases but instead only Foo (where there is the decorator but no InitVar) and Qux (where there is no decorator but there is an InitVar) work.

There is a, at least conceptually, related problem with input processing in the presence of an InitVar with an example in issue #582 .

Example

"""
Example of InitVar causing problems with metadata when dataclass decorated with @serde
"""

from dataclasses import dataclass, InitVar, field

from serde import serde
from serde import field as serde_field
from serde.json import to_json

# apply workaround from https://github.com/yukinarit/pyserde/issues/425#top
InitVar.__call__ = lambda *args: None

@serde
@dataclass
class Foo:
    a: int = serde_field(skip=True)

@serde
@dataclass
class Bar:
    a: int = serde_field(skip=True)
    b: InitVar[int]

@serde
@dataclass
class Baz:
    a: int = field(metadata={"serde_skip": True})
    b: InitVar[int]

@dataclass
class Qux:
    a: int = field(metadata={"serde_skip": True})
    b: InitVar[int]

def get_meta(dc, f):
    return dc.__dataclass_fields__[f].metadata

for test in [Foo(1), Bar(1, 2), Baz(1, 2), Qux(1, 2)]:
    print(f'{test=} {get_meta(test, "a")=} found: {get_meta(test, "a") == {"serde_skip": True}}')

print("Foo", to_json(Foo(1)))
print("Bar", to_json(Bar(1, 2)))
print("Baz", to_json(Baz(1, 2)))
print("Qux", to_json(Qux(1, 2)))

Output

test=Foo(a=1) get_meta(test, "a")=mappingproxy({'serde_skip': True}) found: True
test=Bar(a=1) get_meta(test, "a")=mappingproxy({}) found: False
test=Baz(a=1) get_meta(test, "a")=mappingproxy({}) found: False
test=Qux(a=1) get_meta(test, "a")=mappingproxy({'serde_skip': True}) found: True
Foo {}
Bar {"a":1}
Baz {"a":1}
Qux {}

Environment Details


 Serde: 0.20.0
 Python 3.11.9 
 Os: macOS-12.7.5-arm64-arm-64bit
 Machine: arm64
yukinarit commented 4 weeks ago

Interesting. If I remove @dataclass decorator. It looks like this

test=Foo(a=1) get_meta(test, "a")=mappingproxy({'serde_skip': True}) found: True
test=Bar(a=1) get_meta(test, "a")=mappingproxy({'serde_skip': True}) found: True
test=Baz(a=1) get_meta(test, "a")=mappingproxy({'serde_skip': True}) found: True
test=Qux(a=1) get_meta(test, "a")=mappingproxy({'serde_skip': True}) found: True
Foo {}
Bar {}
Baz {}
Qux {}
yukinarit commented 4 weeks ago

Hi @morrison12 I made #584 . Can you please check?

morrison12 commented 4 weeks ago

Hello @yukinarit. That solves it in both cases.