Как я могу напечатать подсказку динамически установленного атрибута класса в метаклассе? - PullRequest
0 голосов
/ 01 марта 2019

Когда я динамически устанавливаю атрибут класса:

from typing import TypeVar, Generic, Optional, ClassVar, Any

class IntField:
    type = int

class PersonBase(type):
    def __new__(cls):
        for attr, value in cls.__dict__.items():
            if not isinstance(value, IntField):
                continue
            setattr(cls, attr, value.type())
        return cls

class Person(PersonBase):
    age = IntField()

person = Person()

print(type(Person.age)) # <class 'int'>
print(type(person.age)) # <class 'int'>
person.age = 25 # Incompatible types in assignment (expression has type "int", variable has type "IntField")

Тип атрибута age будет иметь тип int, но MyPy не может следовать за этим.

Есть ли способ заставить MyPy понять?

В Django это реализовано:

from django.db import models

class Person(models.Model):
    age = models.IntegerField()

person = Person()
print(type(Person.age)) # <class 'django.db.models.query_utils.DeferredAttribute'>
print(type(person.age)) # <class 'int'>
person.age = 25  # No error

Как Django делает это?

Ответы [ 2 ]

0 голосов
/ 01 марта 2019

Поскольку вы определяете поле в классе, практический подход заключается в подсказке типа поля.Обратите внимание, что вы должны указать mypy не проверять саму строку.

class Person(PersonBase):
    age: int = IntField()  # type: ignore

Это наименьшее изменение, но довольно негибкое.


Вы можете создавать автоматически набранные, общие подсказки, используя вспомогательную функцию с поддельной подписью:

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())

Вот так библиотека attrib предоставляет свои устаревшие подсказки.Этот стиль позволяет скрыть всю магию / хаки аннотаций.


Поскольку метакласс может проверять аннотации, нет необходимости хранить тип в поле.Вы можете использовать только Field для метаданных и аннотацию для типа:

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()

Так attrib предоставляет свои атрибуты Python 3.6+.Он является общим и соответствует стилю аннотации.Обратите внимание, что это также может использоваться с обычным базовым классом вместо метакласса.

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()
0 голосов
/ 01 марта 2019

Патрик Хау прав, я пытаюсь решить это неправильно.Дескрипторы - это путь:

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 может полностью понять это.Спасибо!

...