biqqles / dataclassy

A fast and flexible reimplementation of data classes
https://pypi.org/project/dataclassy
Mozilla Public License 2.0
81 stars 9 forks source link

Positional init-only arguments? #33

Closed nolar closed 3 years ago

nolar commented 3 years ago

First of all, thanks for this great library. I like how it makes data classes easier than the built-in dataclasses, and especially the support for __slots__.

While switching my framework to dataclassy, I've hit one problem that I cannot express in code properly:

How can I declare pseudo-positional InitVars?

Here is the equivalent code for dataclasses:

import dataclasses

@dataclasses.dataclass()
class Selector:
    arg1: dataclasses.InitVar[Union[None, str, Marker]] = None
    arg2: dataclasses.InitVar[Union[None, str, Marker]] = None
    arg3: dataclasses.InitVar[Union[None, str, Marker]] = None
    argN: dataclasses.InitVar[None] = None  # a runtime guard against too many positional arguments

    group: Optional[str] = None
    version: Optional[str] = None
    plural: Optional[str] = None
    # ... more things here

    def __post_init__(
            self,
            arg1: Union[None, str, Marker],
            arg2: Union[None, str, Marker],
            arg3: Union[None, str, Marker],
            argN: None,  # a runtime guard against too many positional arguments
    ) -> None:
        ...

The supposed use-case is:

# All notations are equivalent and must create exactly the same objects:
CRDS = Selector('apiextensions.k8s.io', 'customresourcedefinitions')
CRDS = Selector('apiextensions.k8s.io', plural='customresourcedefinitions')
CRDS = Selector('apiextensions.k8s.io', None, plural='customresourcedefinitions')
CRDS = Selector('apiextensions.k8s.io', version=None, plural='customresourcedefinitions')
CRDS = Selector(group='apiextensions.k8s.io', version=None, plural='customresourcedefinitions')

I.e., it is either explicitly specifying the kwargs to be stored on the data class, or passing them as positional (pseudo-positional) init-vars. The positional init-vars "arg1..arg3" are then interpreted in the post-init method to be stored as one of the group/version/plural/etc fields as it seems appropriate. In some cases, it might even parse and split positional init-args into several fields: e.g. Selector('apiextensions.k8s.io/v1') would be split to Selector(group='apiextensions.k8s.io', version='v1').

The details are not essential, the only essential part here is the post-init contains "some logic" for converting these pseudo-positional arg1..argN into the actual useful storeable fields.

For the full example:


When I try to do this the dataclassy-way, and remove the InitVar[] declarations, the positional arguments go to the first fields: e.g., group & version for the first example line, while the intention is to interpret them as group & plural (as it would be implemented in the post-init function) — which expectedly gives wrong results.

If I keep the arg1..argN fields in the top of the list of fields, they are accepted as needed but are stored on the object as the same-named fields. I can make them internal and hide them from reprs, but I would prefer to not store them at all and keep them as init-only.

What would be the best way to implement the positional init-only variables with dataclassy?

Thank you in advance.

biqqles commented 3 years ago

Since you are doing so much in __post_init__ anyway, it might be simplest to just use init=False and write a custom __init__ instead of using the generated method.

nolar commented 3 years ago

Thanks! Indeed, I somehow managed to miss this option :-)