Home > OS >  Calling a dataclass constructor with decorator given only a type object
Calling a dataclass constructor with decorator given only a type object

Time:01-31

I have a dataclass which inherits an abstract class that implements some boilerplate, and also uses the @validate_arguments decorator to immediately cast strings back into numbers on object creation. The dataclass is a series of figures, some of which are calculated in the __post_init__.

report.py:

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pydantic import validate_arguments


@dataclass
class Report(ABC):
    def __post_init__(self):
        self.process_attributes()

    @abstractmethod
    def process_attributes(self):
        pass


@validate_arguments
@dataclass
class SpecificReport(Report):
    some_number: int
    some_other_number: float
    calculated_field: float = field(init=False)

    def process_attributes(self):
        self.calculated_field = self.some_number * self.some_other_number

I then have another class which is initialized with a class of type Report, gathers some metadata on creation about that class, and then has methods which perform operations with these objects, including taking some content and then constructing new objects of this type from a dictionary. We determine which fields are set explicitly with inspect.signature and explode out our dictionary and call the constructor.

report_editor.py

from inspect import signature

from report import Report, SpecificReport


class ReportEditor:
    def __init__(self, report_type: type[Report], content=None):
        self.content = content
        self.report_type = report_type
        self.explicit_fields = list(signature(report_type).parameters.keys())

    def process(self):
        initializable_dict = {key: val for key, val in self.content.items() if key in self.explicit_fields}
        report = self.report_type(**initializable_dict)
        print(report)

However, this produces an error when hitting process_attributes, because the validate_arguments step is not performed. Aside from that, the object is initialized as I'd expect, but since the values are strings, they remain as such and only throw an exception once trying to do an operation.

This works just fine and produces the desired behavior:

    def process(self):
        initializable_dict = {key: val for key, val in self.content.items() if key in self.explicit_fields}
        report = SpecificReport(**initializable_dict)
        print(report)

but, of course, the intent is to abstract that away and allow this ReportEditor class to be able to do these operations without knowing what kind of Report it is.

here is main.py to run the reproducible example:

from report import SpecificReport
from report_editor import ReportEditor


def example():
    new_report = SpecificReport(1, 1.0)
    report_editor = ReportEditor(type(new_report), {
            "some_number": "1",
            "some_other_number": "1.0",
            "calculated_field": "1.0"
        })
    report_editor.process()


if __name__ == '__main__':
    example()

I tried putting @validate_arguments on both the parent and child classes, as well as only on the parent Report class. These both resulted in a TypeError: cannot create 'cython_function_or_method' instances. I'm not finding any other way there is to call the constructor from outside just using the type object.

Why is the constructor called properly, but not the decorator function in this instance? Is it possible to maybe cast a type object to a Callable in order to get the full constructor somehow? What am I missing? Or is this just not possible (maybe with generics)?

CodePudding user response:

Here is the fundamental problem:

In [1]: import report

In [2]: new_report = report.SpecificReport(1, 1.0)

In [3]: type(new_report) is report.SpecificReport
Out[3]: False

This is happening because the pydantic.validate_arguments decorator returns a cythonized function:

In [4]: report.SpecificReport
Out[4]: <cyfunction SpecificReport at 0x1103bb370>

The function does the validation. The class constructor doesn't. It looks like this decorator is experimental, and at least for now, is not designed to work on classes (it just happens to work since a class is just a callable with .__annotations__ after all).

EDIT:

However, if you do want validation, you can use pydantic.dataclasses, which is a "drop-in" (not quite but drop-in by very close and they made a real effort at compatibility) replacement for the standard library dataclasses. You can use change the report.py to the following:

from abc import ABC, abstractmethod
import dataclasses
import pydantic

@pydantic.dataclasses.dataclass
class Report(ABC):
    def __post_init_post_parse__(self, *args, **kwargs):
        self.process_attributes()

    @abstractmethod
    def process_attributes(self, *args, **kwargs):
        pass


@pydantic.dataclasses.dataclass
class SpecificReport(Report):
    some_number: int
    some_other_number: float
    calculated_field: dataclasses.InitVar[float] = dataclasses.field(init=False)

    def process_attributes(self, *args, **kwargs):
        self.calculated_field = self.some_number * self.some_other_number

Some subtleties:

  • in __post_init__, the arguments haven't been parsed and validated, but you can use __post_init_post_parse__ if you want them validated/parsed. We do, or else self.some_number * self.some_other_number will raise the TypeError
  • Have to use dataclasses.InitVar along with dataclasses.field(init=False) because without InitVar, the validation fails if __post_init__ didn't set calculated_field (so we can't use the parsed fields in __post_init_post_parse__ because the missing attribute is checked earlier). There might be a way to prevent it from enforcing that, but this is what I found for now. I'm not very comfortable with it. Hopefully someone can find a better way.
  • had to use *args, **kwargs in __post_init_post_parse__ and in process because InitVar will pass an argument, so extenders of this class might want to do the same, so make it generic.

I had to add **args, **kwargs to the

  • Related