Skip to content

Commit a9ed9b0

Browse files
authored
Start reworking pep introduction (#43)
1 parent 3ccd6f1 commit a9ed9b0

9 files changed

Lines changed: 696 additions & 135 deletions

File tree

pre-pep.rst

Lines changed: 213 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,210 @@ case of dataclass-like transformations (:pep:`PEP 681 <681>`).
3737

3838
Examples: 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+
40244
Automatically 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
166370
initializer).
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-
265373
dataclasses-style method generation
266374
-----------------------------------
267375

@@ -275,6 +383,11 @@ This kind of pattern is widespread enough that :pep:`PEP 681 <681>`
275383
was created to represent a lowest-common denominator subset of what
276384
existing 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-
318425
Rationale
319426
=========
320427

@@ -354,6 +461,8 @@ Reference Implementation
354461
Rejected 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

Comments
 (0)