How can I type hint a dynamically set class attribute in a metaclass?
Since you define the field on the class, the practical approach is to type-hint the field. Note that you must tell mypy
to not check the line itself, though.
class Person(PersonBase):
age: int = IntField() # type: ignore
This is the least change, but rather inflexible.
You can create automatically typed, generic hints by using a helper function with a fake signature:
from typing import Type, TypeVar
T = TypeVar('T')
class __Field__:
"""The actual field specification"""
def __init__(self, *args, **kwargs):
self.args, self.kwargs = args, kwargs
def Field(tp: Type[T], *args, **kwargs) -> T:
"""Helper to fake the correct return type"""
return __Field__(tp, *args, **kwargs) # type: ignore
class Person:
# Field takes arbitrary arguments
# You can @overload Fields to have them checked as well
age = Field(int, True, object())
This is how the attrs
library provides its legacy hints. This style allows to hide all the magic/hacks of the annotations.
Since a metaclass can inspect annotations, there is no need to store the type on the Field. You can use a bare Field
for metadata, and an annotation for the type:
from typing import Any
class Field(Any): # the (Any) part is only valid in a .pyi file!
"""Field description for Any type"""
class MetaPerson(type):
"""Metaclass that creates default class attributes based on fields"""
def __new__(mcs, name, bases, namespace, **kwds):
for name, value in namespace.copy().items():
if isinstance(value, Field):
# look up type from annotation
field_type = namespace['__annotations__'][name]
namespace[name] = field_type()
return super().__new__(mcs, name, bases, namespace, **kwds)
class Person(metaclass=MetaPerson):
age: int = Field()
This is how the attrs
provides its Python 3.6+ attributes. It is both generic and conforming to annotation style. Note that this can also be used with a regular baseclass instead of a metaclass.
class BasePerson:
def __init__(self):
for name, value in type(self).__dict__.items():
if isinstance(value, Field):
field_type = self.__annotations__[name]
setattr(self, name, field_type())
class Person(BasePerson):
age: int = Field()
Patrick Haugh is right, I am trying to solve this the wrong way. Descriptors are the way to go:
from typing import TypeVar, Generic, Optional, ClassVar, Any, Type
FieldValueType = TypeVar('FieldValueType')
class Field(Generic[FieldValueType]):
value_type: Type[FieldValueType]
def __init__(self) -> None:
self.value: FieldValueType = self.value_type()
def __get__(self, obj, objtype) -> 'Field':
print('Retrieving', self.__class__)
return self
def __set__(self, obj, value):
print('Updating', self.__class__)
self.value = value
def to_string(self):
return self.value
class StringField(Field[str]):
value_type = str
class IntField(Field[int]):
value_type = int
def to_string(self):
return str(self.value)
class Person:
age = IntField()
person = Person()
person.age = 25
print(person.age.to_string())
MyPy
can fully understand this. Thanks!