|
1 | | -# in stub file |
| 1 | +# https://github.com/python/typing/issues/548#issuecomment-540830100 |
| 2 | +# |
| 3 | +# Use case from syastrov based on Django's ORM `Field`: |
| 4 | +# In Django models, fields are defined as class descriptors (e.g. `CharField(null=True)`). |
| 5 | +# The descriptor's `__get__` returns the underlying Python type (`str`, `int`, etc.), |
| 6 | +# or `type | None` if `null=True`. |
| 7 | +# |
| 8 | +# Without HKT: |
| 9 | +# In standard Python, `Field` cannot specialize its return type in `__new__` across |
| 10 | +# subclasses: |
| 11 | +# `@overload def __new__(cls: type[FieldT], null: Literal[True]) -> FieldT[GT | None]` |
| 12 | +# fails because `FieldT` is a concrete TypeVar and cannot be applied as `FieldT[GT | None]`. |
| 13 | +# Django stubs were forced to require dedicated Mypy/Pyright compiler plugins |
| 14 | +# (`django-stubs`) just to synthesize nullable types for fields when `null=True`. |
| 15 | +# |
| 16 | +# With HKT: |
| 17 | +# `Field` defines overloaded constructor specialization once on `__new__`: |
| 18 | +# `cls: type[FieldT[GT]] -> FieldT[GT | None]`. Subclasses (`CharField`, `IntegerField`) |
| 19 | +# inherit this behavior automatically without plugin magic or redundant overrides. |
2 | 20 |
|
3 | 21 | from __future__ import annotations |
4 | 22 |
|
5 | 23 | from typing import Any, Literal, overload, reveal_type |
6 | 24 |
|
7 | 25 |
|
8 | 26 | class Field[GT]: |
9 | | - # on the line after the overload: error: Type variable "_T" used with arguments |
| 27 | + def __init__(self, null: bool = False, default: Any = None) -> None: |
| 28 | + self.null = null |
| 29 | + self.default = default |
| 30 | + self.name: str = "" |
| 31 | + |
| 32 | + def __set_name__(self, owner: type, name: str) -> None: |
| 33 | + self.name = name |
| 34 | + |
| 35 | + # Standard descriptor protocol: instance access returns GT |
| 36 | + def __get__(self, instance: object, owner: Any) -> GT: |
| 37 | + if instance is None: |
| 38 | + return self # type: ignore |
| 39 | + return getattr(instance, f"_{self.name}", self.default) |
| 40 | + |
| 41 | + def __set__(self, instance: object, value: GT) -> None: |
| 42 | + setattr(instance, f"_{self.name}", value) |
| 43 | + |
| 44 | + # HKT constructor specialization on __new__: |
| 45 | + # Instantiating any Field subclass with null=True wraps the inner element type GT -> GT | None |
| 46 | + # while preserving the exact field subclass (CharField, IntegerField, etc.) |
10 | 47 | @overload |
11 | | - def __new__[FieldT: Field[GT]](cls: type[FieldT[GT]], null: Literal[False] = False, *args: Any, **kwargs: Any) -> FieldT[GT]: ... |
| 48 | + def __new__[FieldT: Field[GT]]( |
| 49 | + cls: type[FieldT[GT]], |
| 50 | + null: Literal[False] = False, |
| 51 | + *args: Any, |
| 52 | + **kwargs: Any, |
| 53 | + ) -> FieldT[GT]: ... |
| 54 | + |
12 | 55 | @overload |
13 | | - def __new__[FieldT: Field[GT]](cls: type[FieldT[GT]], null: Literal[True], *args: Any, **kwargs: Any) -> FieldT[GT | None]: ... |
14 | | - def __new__(cls: Any, null: Any = False, *args: Any, **kwargs: Any) -> Any: ... |
15 | | - def __get__(self, instance: Any, owner: Any) -> GT: ... |
| 56 | + def __new__[FieldT: Field[GT]]( |
| 57 | + cls: type[FieldT[GT]], |
| 58 | + null: Literal[True], |
| 59 | + *args: Any, |
| 60 | + **kwargs: Any, |
| 61 | + ) -> FieldT[GT | None]: ... |
| 62 | + |
| 63 | + def __new__(cls: Any, *args: Any, **kwargs: Any) -> Any: |
| 64 | + return super().__new__(cls) |
| 65 | + |
16 | 66 |
|
17 | | -class CharField(Field[str]): ... |
18 | | -class IntegerField(Field[int]): ... |
19 | | -# etc... |
| 67 | +class CharField(Field[str]): |
| 68 | + pass |
| 69 | + |
| 70 | + |
| 71 | +class IntegerField(Field[int]): |
| 72 | + pass |
20 | 73 |
|
21 | | -# in code |
22 | 74 |
|
23 | 75 | class User: |
24 | | - f1 = CharField(null=False) |
25 | | - f2 = CharField(null=True) |
| 76 | + username = CharField(null=False) |
| 77 | + bio = CharField(null=True) |
| 78 | + age = IntegerField(null=True) |
| 79 | + score = IntegerField(null=False) |
| 80 | + |
| 81 | + |
| 82 | +# 1. Non-nullable field instances return non-optional types |
| 83 | +reveal_type(User().username, expected_text="str") |
| 84 | +reveal_type(User().score, expected_text="int") |
| 85 | + |
| 86 | +# 2. Nullable field instances return Optional types |
| 87 | +reveal_type(User().bio, expected_text="str | None") |
| 88 | +reveal_type(User().age, expected_text="int | None") |
| 89 | + |
| 90 | +# 3. Runtime functional verification |
| 91 | +u = User() |
| 92 | +u.username = "alice" |
| 93 | +u.bio = None |
| 94 | +u.age = 30 |
| 95 | +u.score = 100 |
26 | 96 |
|
27 | | -reveal_type(User().f1, expected_text="str") |
28 | | -reveal_type(User().f2, expected_text="str | None") |
| 97 | +assert u.username == "alice" |
| 98 | +assert u.bio is None |
| 99 | +assert u.age == 30 |
| 100 | +assert u.score == 100 |
0 commit comments