3

Python: 3.7+

I have a dataclass and a subclass of it as following:

from abc import ABC
from dataclasses import dataclass
from typing import Dict, List, Optional

from dbconn import DBConnector


@dataclass
class User:
  uid: int
  name: str


@dataclass
class Model(ABC):
  database: DBConnector
  user: User

  def func(self, *args, **kwargs):
    pass


@dataclass
class Command(Model):
  message: Optional[str] = "Hello"

  def __post_init__(self):
    self.user_id: str = str(self.user.uid)
    self.message = f"{self.user.name}: {self.message}"

I could get the type hint for database, user and message using typing.get_type_hints(Command). How can I get the type hints for user_id?

One workaround would to be pass in the user.uid and user.name as separate params to Command but that's not pragmatic when User object has many useful attributes.

I believe the reason why it doesn't work in the first place is because init gets called at runtime and that's why type checking doesn't take those attrs into account. One possible solution would be to parse the ast of the class but I'm not sure if that's recommended and generic enough. If yes, would appreciate a working example.

Harshith Thota
  • 856
  • 8
  • 20
  • Why not just "declare" it outside `__post_init__`, just like you are doing in `User`? – DeepSpace Jun 16 '21 at 14:04
  • @DeepSpace, That won't be scalable though, I feel. Moreover, I'm trying to create something using those type hints - an ORM of sorts. I don't think declaring attributes for models outside all the time would not be ideal. – Harshith Thota Jun 16 '21 at 19:35

1 Answers1

0

Figured out a hacky solution by using inspect.get_source and regex matching Type Hinted attributes. Also had to convert dataclass into a normal class for the end Model.

from abc import ABC
from dataclasses import dataclass
import inspect
import re
from typing import Dict, List, Optional

from dbconn import DBConnector


@dataclass
class User:
    uid: int
    name: str


@dataclass
class Model(ABC):
    database: DBConnector
    user: User

    def func(self, *args, **kwargs):
        pass

    def get_type_hints(self):
        source = inspect.getsource(self.__class__)
        # Only need type hinted attributes
        patt = r"self\.(?P<name>.+):\s(?P<type>.+)\s="
        attrs = re.findall(patt, source)
        for attr in attrs:
            yield attr + (getattr(self, attr[0]), )


class Command(Model):
    message: Optional[str] = "Hello"

    def __init__(
        self, database: DBConnector,
        user: User,
        message: Optional[str] = "Hello"
    ):
        super().__init__(database, user)
        self.user_id: str = str(self.user.uid)
        self.message: Optional[str] = f"{self.user.name}: {self.message}"


cmd = Command(DBConnector(), User(123, 'Stack Overflow'))
for attr in cmd.get_type_hints():
    print(attr)

# Output
('user_id', 'str', '123')
('message', 'str', 'Stack Overflow: Hello')

If someone can come up with a more robust solution, I'm definitely interested in it. For now, I'll mark this as my answer, in case someone stumbles upon this and is ok with a hacky solution.

Harshith Thota
  • 856
  • 8
  • 20
  • 1
    I can't really improve on this, but it's worth noting that it could be much more efficient to compile a regex once (e.g. `regex = re.compile(patt)`), either in the global namespace or as a class variable, and then call `regex.findall(source)` in your `Model.get_type_hints` method. `re.findall(patt, source)` is having to compile the pattern every time `Model.get_type_hints` called. – Alex Waygood Jul 24 '21 at 22:11