Skip to content

Commit 05d80dd

Browse files
committed
Flesh out Django Field descriptor HKT prototype
- Document context from python/typing#548 comment 540830100 regarding Django ORM Field descriptors and why django-stubs historically required compiler plugins. - Implement descriptor protocol (__init__, __set_name__, __get__, __set__) on base Field with live instance attribute storage. - Define overloaded __new__ constructor specialization once on Field to map null=True to FieldT[GT | None] while preserving subclass types. - Add concrete CharField and IntegerField subclasses demonstrating automatic inheritance of nullable specialization on a User model. - Add reveal_type expectations for non-null and nullable fields alongside runtime functional assertions.
1 parent 3e6004c commit 05d80dd

1 file changed

Lines changed: 86 additions & 14 deletions

File tree

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,100 @@
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.
220

321
from __future__ import annotations
422

523
from typing import Any, Literal, overload, reveal_type
624

725

826
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.)
1047
@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+
1255
@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+
1666

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
2073

21-
# in code
2274

2375
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
2696

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

Comments
 (0)