0
votes

I am creating a decorator that has a similar purpose to that of the builtin dataclass decorator. Given @jsbueno's answer to this question, it would be reasonable to assume that it is not possible to type-hint such a class. However, VS Code is able to supply type hints for dataclasses. Is this hardcoded into VS Code, or is there a way to use the same method dataclass uses in order to get type hinting on my class?

My current implementation of this decorator:

def json_object_factory(cls: type) -> Type[JsonObject]:

    elements: Dict[str, JsonElement] = {}

    # Methods for this object
    def __init__(self):
        for name, elem_dat in elements.items():
            if elem_dat.default is not None:
                setattr(self, name, elem_dat.default)
            else:
                setattr(self, name, elem_dat.default_factory())

    def to_json(self) -> dict:
        json = {}
        for name, elem_dat in elements.items():
            value = getattr(self, name)

            # Run checks on the value
            if elem_dat.expected_type is not None:
                type_check(name, value, elem_dat.expected_type, 
                    not elem_dat.must_exist)
            if elem_dat.bounds is not None:
                low, high = elem_dat.bounds
                bounds_check(name, value, low, high)

            # Maps the element to its new value
            value = elem_dat.mapping(value)

            # Adds it to the dict
            if elem_dat.alternate_name is not None:
                json[elem_dat.alternate_name] = value
            else:
                json[name] = value

        return json

    # Any JsonElement class variables are meant to be instance variables of this
    # new class.
    for name in dir(cls):
        field = getattr(cls, name)

        # Check if the field is of the type we're interested in
        if isinstance(field, JsonElement):
            # Add it to the dictionary
            elements[name] = field

    supers = list(cls.__mro__)[1:] # Slice out the reference to the regular class
    if JsonObject not in supers:
        supers.append(JsonObject)

    return type(cls.__name__, tuple(supers), {
        '__init__': __init__,
        'to_json': to_json
    })

Which uses these classes:

from abc import ABC, abstractmethod
from typing import Any, Callable, Dict, Tuple, Type, Union
from my_other_code import type_check, bounds_check

class JsonObject(ABC):
    
    @abstractmethod
    def to_json(self):
        pass

class JsonElement:
    def __init__(
        self,
        expected_type: type = None,
        default: Any = None, *,
        default_factory: Callable[[], Any] = None,
        mapping: Callable[[Any], Any] = lambda x: x,
        bounds: Tuple[Union[int,float], Union[int,float]] = None,
        must_exist: bool = False,
        alternate_name: str = None
    ):
        self.expected_type = expected_type
        if default is not None and default_factory is not None:
            raise TypeError('Both default and default_factory cannot be supplied.')
        self.default = default
        self.default_factory = default_factory
        self.mapping = mapping
        self.bounds = bounds
        self.must_exist = must_exist
        self.alternate_name = alternate_name

Now, using this module as below works, but to no one's suprise, the type checker has no idea the type or the attributes of the class.

from above import *

@json_object_factory
class Test:
    a = JsonElement(bool, False)
    b = JsonElement(str, 'default')
    c = JsonElement(list, default_factory=list)
Test().a # Typechecker gives an error: JsonObject has no attribute a

But by some witchcraft, the type checker does understand the below code!

from dataclasses import dataclass, field

@dataclass
class Test:
    a: bool = field(default=False)
    b: str = field(default='default')
    c: list = field(default_factory=list)
Test().a # Typechecker recognizes existance of attribute and type of a.

Is it possible to acquire static type-checking like dataclass achieves with my json_object_factory decorator?

Yes, tools are coded to handle dataclass specially. Statically speaking, a is just a class attribute, because the decorator hasn't yet been applied. Furthermore, taking only statically available information, the data class wouldn't type check, because a is annotated as a bool but assigned a field value.chepner