NowanIlfideme / pydantic-yaml

YAML support for Pydantic models
MIT License
138 stars 11 forks source link

Add support for dataclasses #32

Closed LaurentBergeron closed 1 year ago

LaurentBergeron commented 2 years ago

It would be nice to add support for dataclasses

For example:

from pydantic.dataclasses import dataclass
from pydantic_yaml import YamlModel

@dataclass
class SomeDataclass:
    a: int

class SomeModel(YamlModel):
    some_dataclass: SomeDataclass

dc = SomeDataclass(1)
y = SomeModel(some_dataclass=dc).yaml()

currently raises with:

  File "[...]\min.py", line 15, in <module>
    y = SomeModel(some_dataclass=dc).yaml()
  File "[...]\venv\lib\site-packages\pydantic_yaml\mixin.py", line 165, in yaml
    return cfg.yaml_dumps(
  File "[...]\venv\lib\site-packages\pydantic_yaml\compat\yaml_lib.py", line 96, in yaml_safe_dump
    ruamel_obj.dump(data, stream=text_stream)
  File "[...]\venv\lib\site-packages\ruamel\yaml\main.py", line 574, in dump
    return self.dump_all([data], stream, transform=transform)
  File "[...]\venv\lib\site-packages\ruamel\yaml\main.py", line 583, in dump_all
    self._context_manager.dump(data)
  File "[...]\venv\lib\site-packages\ruamel\yaml\main.py", line 915, in dump
    self._yaml.representer.represent(data)
  File "[...]\venv\lib\site-packages\ruamel\yaml\representer.py", line 80, in represent
    node = self.represent_data(data)
  File "[...]\venv\lib\site-packages\ruamel\yaml\representer.py", line 103, in represent_data
    node = self.yaml_representers[data_types[0]](self, data)
  File "[...]\venv\lib\site-packages\ruamel\yaml\representer.py", line 321, in represent_dict
    return self.represent_mapping('tag:yaml.org,2002:map', data)
  File "[...]\venv\lib\site-packages\ruamel\yaml\representer.py", line 214, in represent_mapping
    node_value = self.represent_data(item_value)
  File "[...]\venv\lib\site-packages\ruamel\yaml\representer.py", line 113, in represent_data
    node = self.yaml_representers[None](self, data)
  File "[...]\venv\lib\site-packages\ruamel\yaml\representer.py", line 354, in represent_undefined
    raise RepresenterError(_F('cannot represent an object: {data!s}', data=data))
ruamel.yaml.representer.RepresenterError: cannot represent an object: SomeDataclass(a=1)
NowanIlfideme commented 2 years ago

Main issue with implementing this is registering "representers" for dataclasses... since both PyYAML and Ruamel YAML don't allow implicitly passing subclasses, this is a problem that would require one of the following:

  1. Registering representers for all dataclasses dynamically (by monkeypatching @dataclass, which is suuuuper hacky...)
  2. Going down the "tree" of .dict() results and converting dataclasses into their YamlModel equivalents (oof!).
  3. Creating a custom YAML dumping and parsing engine 🙃

I don't particularly like the 1st fix, because it's very fragile and requires a certain import order (i.e. you must always import pydantic_yaml within your dataclass definitions). 1st and 2nd solution still don't fix parsing these (I'm sure there would be issues, ha). And the 3rd solution is what seems like I'll have to do eventually, but it needs to be well-designed and well-tested, which I do not have time for. :(

Gladly taking any suggestions or ideas I might have missed, though!

LaurentBergeron commented 2 years ago

I wonder if we could use this: https://yaml.readthedocs.io/en/latest/dumpcls.html

It would work something like this:

  1. Wrap pydantic_yaml.dataclasses.dataclass around pydantic.dataclasses.dataclass
  2. In the wrapper, register the class with the YAML() instance
  3. In the wrapper, add to_yaml and from_yaml methods to the dataclass (not even sure this would be necessary, but the option is there just in case)
NowanIlfideme commented 1 year ago

Note that you can now do this with json_encoders (Pydantic v1) or @field_serializer (Pydantic v2) for specific fields.

With Pydantic v2 you can also use RootModel to dump your model:

obj = YourType()
to_yaml_str(RootModel[YourType](obj))

I'll add this to docs.

NowanIlfideme commented 1 year ago

Added to docs. Top-level available since version 1.1.0 🎉