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
,
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,
And if I do pass parameters, I actually get an Unexpected Argument
warning,
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
From python 3.7 above, you can achieve same effect by using @dataclass
and adding appropriate typehint to the instance fields.