@@ -37,6 +37,210 @@ case of dataclass-like transformations (:pep:`PEP 681 <681>`).
3737
3838Examples: pydantic/fastapi, dataclasses, sqlalchemy
3939
40+ Prisma-style ORMs
41+ -----------------
42+
43+ `Prisma <#prisma _>`_, a popular ORM for TypeScript, allows writing
44+ queries like (adapted from `this example <#prisma-example _>`_)::
45+
46+ const user = await prisma.user.findMany({
47+ select: {
48+ name: true,
49+ email: true,
50+ posts: true,
51+ },
52+ });
53+
54+ for which the inferred type will be something like::
55+
56+ {
57+ email: string;
58+ name: string | null;
59+ posts: {
60+ id: number;
61+ title: string;
62+ content: string | null;
63+ authorId: number | null;
64+ }[];
65+ }[]
66+
67+ Here, the output type is a combination of both existing information
68+ about the type of ``prisma.user `` and the type of the argument to
69+ ``findMany ``. It returns an array of objects containing the properties
70+ of ``user `` that were requested; one of the requested elements,
71+ ``posts ``, is a "relation" referencing another model; it has *all * of
72+ its properties fetched but not its relations.
73+
74+ We would like to be able to do something similar in Python, perhaps
75+ with a schema defined like::
76+
77+ class Comment:
78+ id: Property[int]
79+ name: Property[str]
80+ poster: Link[User]
81+
82+
83+ class Post:
84+ id: Property[int]
85+
86+ title: Property[str]
87+ content: Property[str]
88+
89+ comments: MultiLink[Comment]
90+ author: Link[Comment]
91+
92+
93+ class User:
94+ id: Property[int]
95+
96+ name: Property[str]
97+ email: Property[str]
98+ posts: Link[Post]
99+
100+ (In Prisma, a code generator generates type definitions based on a
101+ prisma schema in its own custom format; you could imagine something
102+ similar here, or that the definitions were hand written)
103+
104+ and a call like::
105+
106+ db.select(
107+ User,
108+ name=True,
109+ email=True,
110+ posts=True,
111+ )
112+
113+ which would have return type ``list[<User>] `` where::
114+
115+ class <User>:
116+ name: str
117+ email: str
118+ posts: list[<Post>]
119+
120+ class <Post>
121+ id: int
122+ title: str
123+ content: str
124+
125+
126+ Unlike the FastAPI-style example above, we probably don't have too
127+ much need for runtime introspection of the types here, which is good:
128+ inferring the type of a function is much less likely to be feasible.
129+
130+
131+ .. _qb-impl :
132+
133+ Implementation
134+ ''''''''''''''
135+
136+ This will take something of a tutorial approach in discussing the
137+ implementation, and explain the features being used as we use
138+ them. More details were appear in the specification section.
139+
140+ First, to support the annotations we saw above, we have a collection
141+ of dummy classes with generic types.
142+
143+ ::
144+
145+ class Pointer[T]:
146+ pass
147+
148+ class Property[T](Pointer[T]):
149+ pass
150+
151+ class Link[T](Pointer[T]):
152+ pass
153+
154+ class SingleLink[T](Link[T]):
155+ pass
156+
157+ class MultiLink[T](Link[T]):
158+ pass
159+
160+ The ``select `` method is where we start seeing new things.
161+
162+ The ``**kwargs: Unpack[K] `` is part of this proposal, and allows
163+ *inferring * a TypedDict from keyword args.
164+
165+ ``Attrs[K] `` extracts ``Member `` types corresponding to every
166+ type-annotated attribute of ``K ``, while calling ``NewProtocol `` with
167+ ``Member `` arguments constructs a new structural type.
168+
169+ ``GetName `` is a getter operator that fetches the name of a ``Member ``
170+ as a literal type--all of these mechanisms lean very heavily on literal types.
171+ ``GetAttr `` gets the type of an attribute from a class.
172+
173+ ::
174+
175+ def select[ModelT, K: BaseTypedDict](
176+ typ: type[ModelT],
177+ /,
178+ **kwargs: Unpack[K],
179+ ) -> list[
180+ NewProtocol[
181+ *[
182+ Member[
183+ GetName[c],
184+ ConvertField[GetAttr[ModelT, GetName[c]]],
185+ ]
186+ for c in Iter[Attrs[K]]
187+ ]
188+ ]
189+ ]: ...
190+
191+ ConvertField is our first type helper, and it is a conditional type
192+ alias, which decides between two types based on a (limited)
193+ subtype-ish check.
194+
195+ In ``ConvertField ``, we wish to drop the ``Property `` or ``Link ``
196+ annotation and produce the underlying type, as well as, for links,
197+ producing a new target type containing only properties and wrapping
198+ ``MultiLink `` in a list.
199+
200+ ::
201+
202+ type ConvertField[T] = (
203+ AdjustLink[PropsOnly[PointerArg[T]], T] if Sub[T, Link] else PointerArg[T]
204+ )
205+
206+ ``PointerArg `` gets the type argument to ``Pointer `` or a subclass.
207+
208+ ``GetArg[T, Base, I] `` is one of the core primitives; it fetches the
209+ index ``I `` type argument to ``Base `` from a type ``T ``, if ``T ``
210+ inherits from ``Base ``.
211+
212+ (The subtleties of this will be discussed later; in this case, it just
213+ grabs the argument to a ``Pointer ``).
214+
215+ ::
216+
217+ type PointerArg[T: Pointer] = GetArg[T, Pointer, 0]
218+
219+ ``AdjustLink `` sticks a ``list `` around ``MultiLink ``, using features
220+ we've discussed already.
221+
222+ ::
223+
224+ type AdjustLink[Tgt, LinkTy] = list[Tgt] if Sub[LinkTy, MultiLink] else Tgt
225+
226+ And the final helper, ``PropsOnly[T] ``, generates a new type that
227+ contains all the ``Property `` attributes of ``T ``.
228+
229+ ::
230+
231+ type PropsOnly[T] = list[
232+ NewProtocol[
233+ *[
234+ Member[GetName[p], PointerArg[GetType[p]]]
235+ for p in Iter[Attrs[T]]
236+ if Sub[GetType[p], Property]
237+ ]
238+ ]
239+ ]
240+
241+ The full test is `in our test suite <#qb-test _>`_.
242+
243+
40244Automatically deriving FastAPI CRUD models
41245------------------------------------------
42246
@@ -140,15 +344,15 @@ suite, but here is a possible implementation of just ``Public``::
140344 # If it is a Field, then we try pulling out the "default" field,
141345 # otherwise we return the type itself.
142346 type GetDefault[Init] = (
143- GetFieldItem[Init, Literal["default"]] if Sub [Init, Field] else Init
347+ GetFieldItem[Init, Literal["default"]] if IsSub [Init, Field] else Init
144348 )
145349
146350 # Create takes everything but the primary key and preserves defaults
147351 type Create[T] = NewProtocol[
148352 *[
149353 Member[GetName[p], GetType[p], GetQuals[p], GetDefault[GetInit[p]]]
150354 for p in Iter[Attrs[T]]
151- if not Sub [
355+ if not IsSub [
152356 Literal[True], GetFieldItem[GetInit[p], Literal["primary_key"]]
153357 ]
154358 ]
@@ -166,102 +370,6 @@ from a ``default`` argument to a field or specified directly as an
166370initializer).
167371
168372
169- Prisma-style ORMs
170- -----------------
171-
172- `Prisma <#prisma _>`_, a popular ORM for TypeScript, allows writing
173- queries like (adapted from `this example <#prisma-example _>`_)::
174-
175- const user = await prisma.user.findMany({
176- select: {
177- name: true,
178- email: true,
179- posts: true,
180- },
181- });
182-
183- for which the inferred type will be something like::
184-
185- {
186- email: string;
187- name: string | null;
188- posts: {
189- id: number;
190- title: string;
191- content: string | null;
192- authorId: number | null;
193- }[];
194- }[]
195-
196- Here, the output type is a combination of both existing information
197- about the type of ``prisma.user `` and the type of the argument to
198- ``findMany ``. It returns an array of objects containing the properties
199- of ``user `` that were requested; one of the requested elements,
200- ``posts ``, is a "relation" referencing another model; it has *all * of
201- its properties fetched but not its relations.
202-
203- We would like to be able to do something similar in Python, perhaps
204- with a schema defined like::
205-
206- class Comment:
207- id: Property[int]
208- name: Property[str]
209- poster: Link[User]
210-
211-
212- class Post:
213- id: Property[int]
214-
215- title: Property[str]
216- content: Property[str]
217-
218- comments: MultiLink[Comment]
219- author: Link[Comment]
220-
221-
222- class User:
223- id: Property[int]
224-
225- name: Property[str]
226- email: Property[str]
227- posts: Link[Post]
228-
229- (In Prisma, a code generator generates type definitions based on a
230- prisma schema in its own custom format; you could imagine something
231- similar here, or that the definitions were hand written)
232-
233- and a call like::
234-
235- db.select(
236- User,
237- name=True,
238- email=True,
239- posts=True,
240- )
241-
242- which would have return type ``list[<User>] `` where::
243-
244- class <User>:
245- name: str
246- email: str
247- posts: list[<Post>]
248-
249- class <Post>
250- id: int
251- title: str
252- content: str
253-
254-
255- Unlike the FastAPI-style example above, we probably don't have too
256- much need for runtime introspection of the types here, which is good:
257- inferring the type of a function is much less likely to be feasible.
258-
259-
260- Implementation
261- ''''''''''''''
262-
263- We have a more `worked example <#qb-test _>`_ in our test suite.
264-
265373dataclasses-style method generation
266374-----------------------------------
267375
@@ -275,6 +383,11 @@ This kind of pattern is widespread enough that :pep:`PEP 681 <681>`
275383was created to represent a lowest-common denominator subset of what
276384existing libraries do.
277385
386+ .. _init-impl :
387+
388+ Implementation
389+ ''''''''''''''
390+
278391::
279392
280393 # Generate the Member field for __init__ for a class
@@ -309,12 +422,6 @@ existing libraries do.
309422 ]
310423
311424
312- TODO: We still need a full story on *how * best to apply this kind of
313- type modifier to a type. With dataclasses, which is a decorator, we
314- could put it in the decorator type... But what about things that use
315- ``__init_subclass__ `` or even metaclasses?
316-
317-
318425Rationale
319426=========
320427
@@ -354,6 +461,8 @@ Reference Implementation
354461Rejected Ideas
355462==============
356463
464+ * Don't attempt to support runtime evaluation, make
465+
357466[Why certain ideas that were brought while discussing this PEP were not ultimately pursued.]
358467
359468
0 commit comments