Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add from_* functions to parse without inheritance for nested structures #108

Closed
fakuivan opened this issue May 6, 2023 · 5 comments · Fixed by #169
Closed

Add from_* functions to parse without inheritance for nested structures #108

fakuivan opened this issue May 6, 2023 · 5 comments · Fixed by #169
Labels
enhancement New feature or request

Comments

@fakuivan
Copy link

fakuivan commented May 6, 2023

Is your feature request related to a problem? Please describe.
I'd like to switch from dacite to this, but that would entail modifying every class on a structure to inherit from a specific mixin.

Describe the solution you'd like
It'd be great to have functions in the family of from_* that could be called on plain dataclasses to generate a compiled parsing function.

from mashumaro import from_yaml
from dataclasses import dataclass

@dataclass
class MySection:
  field: str

@dataclass
class MyConfig:
  section: MySection

# Compilation happens here, the returned callable parses the contents
config_from_yaml = from_yaml(MyConfig)
with open("./config.yaml") as file:
  config_from_yaml(file.read())

Describe alternatives you've considered
I've tried a more naive approach but it does not work for nested structures.

from mashumaro.mixins.yaml import DataClassYAMLMixin
from typing import TypeVar, Type, Callable
T = TypeVar("T")
def from_yaml(type_: Type[T]) -> Callable[[str], T]:
  @dataclass
  class Inheritancent(type_, DataClassYAMLMixin):
    pass
  return Inheritancent.from_yaml
>>> from mashumaro.mixins.yaml import DataClassYAMLMixin
>>> T = TypeVar("T")
>>> def from_yaml(type_: Type[T]) -> Callable[[str], T]:
...   @dataclass
...   class Inheritancent(type_, DataClassYAMLMixin):
...     pass
...   def parse_from_yaml(contents: str):
...     return Inheritancent.from_yaml(contents)
... 
>>> from dataclasses import dataclass
>>> 
>>> @dataclass
... class MySection:
...   field: str
... 
>>> @dataclass
... class MyConfig:
...   section: MySection
... 
>>> from_yaml(MyConfig)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in from_yaml
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/mixins/dict.py", line 24, in __init_subclass__
    compile_mixin_unpacker(cls, **builder_params["unpacker"])
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/mixin.py", line 49, in compile_mixin_unpacker
    builder.add_unpack_method()
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 398, in add_unpack_method
    self._add_unpack_method_lines(method_name)
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 313, in _add_unpack_method_lines
    self._unpack_method_set_value(
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/code/builder.py", line 440, in _unpack_method_set_value
    unpacked_value = UnpackerRegistry.get(
  File "/home/fakui/.local/lib/python3.10/site-packages/mashumaro/core/meta/types/common.py", line 95, in get
    raise UnserializableField(
mashumaro.exceptions.UnserializableField: Field "section" of type MySection in from_yaml.<locals>.Inheritancent is not serializable
@Fatal1ty Fatal1ty added the enhancement New feature or request label May 10, 2023
@Fatal1ty
Copy link
Owner

This is something I've been asked for many times. I do have plans to do this, but I haven't yet figured out what a practical and customizable method builder should look like. Maybe you have some ideas.

Related issue:

@fakuivan
Copy link
Author

I'm not at all familiar with all the customizations provided by this module, however if I had to hack in support for this I'd start by making a recursive function to swap out all the dataclasses in a dataclass by serializable ones using the method I proposed in the issue post. I would personally not bother much with all the customization options for now since if your use case is very specific you'd probably be better off by doing this the "proper" way and defining the structures yourself.

@Fatal1ty
Copy link
Owner

Fatal1ty commented Jul 24, 2023

I've tried a more naive approach but it does not work for nested structures.

from mashumaro.mixins.yaml import DataClassYAMLMixin
from typing import TypeVar, Type, Callable
T = TypeVar("T")
def from_yaml(type_: Type[T]) -> Callable[[str], T]:
  @dataclass
  class Inheritancent(type_, DataClassYAMLMixin):
    pass
  return Inheritancent.from_yaml

@fakuivan It will work now with #131 merged

A more practical way could be as follows:

from dataclasses import dataclass
from typing import Callable, Type, TypeVar

from mashumaro.core.meta.code.builder import CodeBuilder
from mashumaro.mixins.yaml import default_decoder, default_encoder

T = TypeVar("T")


def from_yaml(type_: Type[T]) -> Callable[[str], T]:
    cb = CodeBuilder(type_, format_name="yaml", decoder=default_decoder)
    cb.add_unpack_method()
    return getattr(type_, "__mashumaro_from_yaml__")


@dataclass
class MySection:
    field: str


@dataclass
class MyConfig:
    section: MySection


data = """
section:
  field: foobar
"""


print(from_yaml(MyConfig)(data)). # MyConfig(section=MySection(field='foobar'))

I know it's still a workaround and there's still a few kinks to work out, but it's a start.

@Fatal1ty
Copy link
Owner

Good news everyone! I’m working on it, so this functionality will be a part of 3.11 release.

@Fatal1ty
Copy link
Owner

The long-awaited pull request has landed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants