Search code examples
pythonsqlalchemypycharmpython-typing

Type-hinting for the __init__ function from class meta information in Python


What I'd like to do is replicate what SQLAlchemy does, with its DeclarativeMeta class. With this code,

from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Person(Base):
    __tablename__ = 'person'
    id = Column(Integer, primary_key=True)

    name = Column(String)
    age = Column(Integer)

When you go to create a person in PyCharm, Person(..., you get typing hints about id: int, name: str, age: int,

Type-hinting with Python, PyCharm and SQLAlchemy

How it works at runtime is via the SQLAlchemy's _declarative_constructor functions,

def _declarative_constructor(self, **kwargs):
    cls_ = type(self)
    for k in kwargs:
        if not hasattr(cls_, k):
            raise TypeError(
                "%r is an invalid keyword argument for %s" %
                (k, cls_.__name__))
        setattr(self, k, kwargs[k])
_declarative_constructor.__name__ = '__init__'

And to get the really nice type-hinting (where if your class has a id field, Column(Integer) your constructor type-hints it as id: int), PyCharm is actually doing some under-the-hood magic, specific to SQLAlchemy, but I don't need it to be that good / nice, I'd just like to be able to programatically add type-hinting, from the meta information of the class.

So, in a nutshell, if I have a class like,

class Simple:
    id: int = 0

    name: str = ''
    age: int = 0

I want to be able to init the class like above, Simple(id=1, name='asdf'), but also get the type-hinting along with it. I can get halfway (the functionality), but not the type-hinting.

If I set things up like SQLAlchemy does it,

class SimpleMeta(type):
    def __init__(cls, classname, bases, dict_):
        type.__init__(cls, classname, bases, dict_)


metaclass = SimpleMeta(
    'Meta', (object,), dict(__init__=_declarative_constructor))


class Simple(metaclass):
    id: int = 0

    name: str = ''
    age: int = 0


print('cls', typing.get_type_hints(Simple))
print('init before', typing.get_type_hints(Simple.__init__))
Simple.__init__.__annotations__.update(Simple.__annotations__)
print('init after ', typing.get_type_hints(Simple.__init__))
s = Simple(id=1, name='asdf')
print(s.id, s.name)

It works, but I get no type hinting,

No init type hint

And if I do pass parameters, I actually get an Unexpected Argument warning,

Unexpected Argument

In the code, I've manually updated the __annotations__, which makes the get_type_hints return the correct thing,

cls {'id': <class 'int'>, 'name': <class 'str'>, 'age': <class 'int'>}
init before {}
init after  {'id': <class 'int'>, 'name': <class 'str'>, 'age': <class 'int'>}
1 asdf

Solution

  • From python 3.7 above, you can achieve same effect by using @dataclass and adding appropriate typehint to the instance fields.

    https://docs.python.org/3/library/dataclasses.html

    screenshot of typehint