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
Source code: Lib/typing.py The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc. This module provides runtime support for type hints. The most fundamental support consists of the types Any, Union, Callable , TypeVar, and Generic.
The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc. This module provides runtime support for type hints.
Metaclasses are classes that inherit directly from type. The method that custom metaclasses should implement is the __new__ method. The arguments mentioned in the __new__ method of metaclasses reflects in the __new__ method of type class. It has four positional arguments. They are as follows: The first argument is the metaclass itself.
Here you will get the below error message while trying to inherit from two different metaclasses. This is because Python can only have one metaclass for a class. Here, class C can’t inherit from two metaclasses, which results in ambiguity. In most cases, we don’t need to go for a metaclass, normal code will fit with the class and object.
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
Updating the __annotations__
from __init__
is the correct way to go there. It is possible to do so using a metaclass, classdecorator, or an appropriate __init_subclass__
method on your base classes.
However, PyCharm raising this warning should be treated as a bug in Pycharm itself: Python has documented mechanisms in the language so that object.__new__
will ignore extra arguments on a class instantiation (which is a "class call") if an __init__
is defined in any subclass in the inheritance chain. On yielding this warning, pycharm is actually behaving differently than the language spec.
The work around it is to have the same mechanism that updates __init__
to create a proxy __new__
method with the same signature. However, this method will have to swallow any args itself - so getting the correct behavior if your class hierarchy needs an actual __new__
method somewhere is a complicating edge case.
The version with __init_subclass__
would be more or less:
class Base:
def __init_subclass__(cls, *args, **kw):
super().__init_subclass__(*args, **kw)
if not "__init__" in cls.__dict__:
cls.__init__ = lambda self, *args, **kw: super(self.__class__, self).__init__(*args, **kw)
cls.__init__.__annotations__.update(cls.__annotations__)
if "__new__" not in cls.__dict__:
cls.__new__ = lambda cls, *args, **kw: super(cls, cls).__new__(cls)
cls.__new__.__annotations__.update(cls.__annotations__)
Python correctly updates a class' .__annotations__
attribute upon inheritance, therefore even this simple code works with inheritance (and multiple inheritance) - the __init__
and __new__
methods are always set with the correct annotations even for attributes defined in superclasses.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With