@alloy-js/python 0.5.0-dev.0 → 0.5.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/dist/dev/src/builtins/python.js +46 -0
  2. package/dist/dev/src/builtins/python.js.map +1 -1
  3. package/dist/dev/src/components/ClassDeclaration.js +19 -10
  4. package/dist/dev/src/components/ClassDeclaration.js.map +1 -1
  5. package/dist/dev/src/components/ClassMethodDeclaration.js +20 -5
  6. package/dist/dev/src/components/ClassMethodDeclaration.js.map +1 -1
  7. package/dist/dev/src/components/DataclassDeclaration.js +14 -12
  8. package/dist/dev/src/components/DataclassDeclaration.js.map +1 -1
  9. package/dist/dev/src/components/DecoratorList.js +55 -0
  10. package/dist/dev/src/components/DecoratorList.js.map +1 -0
  11. package/dist/dev/src/components/EnumDeclaration.js +21 -12
  12. package/dist/dev/src/components/EnumDeclaration.js.map +1 -1
  13. package/dist/dev/src/components/FunctionBase.js +21 -10
  14. package/dist/dev/src/components/FunctionBase.js.map +1 -1
  15. package/dist/dev/src/components/FutureStatement.js +1 -1
  16. package/dist/dev/src/components/MethodBase.js +16 -4
  17. package/dist/dev/src/components/MethodBase.js.map +1 -1
  18. package/dist/dev/src/components/PropertyDeclaration.js +68 -17
  19. package/dist/dev/src/components/PropertyDeclaration.js.map +1 -1
  20. package/dist/dev/src/components/PydanticClassDeclaration.js +136 -0
  21. package/dist/dev/src/components/PydanticClassDeclaration.js.map +1 -0
  22. package/dist/dev/src/components/StaticMethodDeclaration.js +19 -5
  23. package/dist/dev/src/components/StaticMethodDeclaration.js.map +1 -1
  24. package/dist/dev/src/components/index.js +1 -0
  25. package/dist/dev/src/components/index.js.map +1 -1
  26. package/dist/dev/test/classdeclarations.test.js +85 -52
  27. package/dist/dev/test/classdeclarations.test.js.map +1 -1
  28. package/dist/dev/test/dataclassdeclarations.test.js +122 -89
  29. package/dist/dev/test/dataclassdeclarations.test.js.map +1 -1
  30. package/dist/dev/test/decoratorlist.test.js +84 -0
  31. package/dist/dev/test/decoratorlist.test.js.map +1 -0
  32. package/dist/dev/test/enums.test.js +41 -10
  33. package/dist/dev/test/enums.test.js.map +1 -1
  34. package/dist/dev/test/functiondeclaration.test.js +81 -61
  35. package/dist/dev/test/functiondeclaration.test.js.map +1 -1
  36. package/dist/dev/test/methoddeclaration.test.js +117 -26
  37. package/dist/dev/test/methoddeclaration.test.js.map +1 -1
  38. package/dist/dev/test/propertydeclaration.test.js +109 -7
  39. package/dist/dev/test/propertydeclaration.test.js.map +1 -1
  40. package/dist/dev/test/pydanticclassdeclarations.test.js +1137 -0
  41. package/dist/dev/test/pydanticclassdeclarations.test.js.map +1 -0
  42. package/dist/src/builtins/python.d.ts +30 -0
  43. package/dist/src/builtins/python.d.ts.map +1 -1
  44. package/dist/src/builtins/python.js +46 -0
  45. package/dist/src/builtins/python.js.map +1 -1
  46. package/dist/src/components/ClassDeclaration.d.ts +21 -0
  47. package/dist/src/components/ClassDeclaration.d.ts.map +1 -1
  48. package/dist/src/components/ClassDeclaration.js +6 -1
  49. package/dist/src/components/ClassDeclaration.js.map +1 -1
  50. package/dist/src/components/ClassMethodDeclaration.d.ts +5 -1
  51. package/dist/src/components/ClassMethodDeclaration.d.ts.map +1 -1
  52. package/dist/src/components/ClassMethodDeclaration.js +14 -3
  53. package/dist/src/components/ClassMethodDeclaration.js.map +1 -1
  54. package/dist/src/components/DataclassDeclaration.d.ts.map +1 -1
  55. package/dist/src/components/DataclassDeclaration.js +10 -4
  56. package/dist/src/components/DataclassDeclaration.js.map +1 -1
  57. package/dist/src/components/DecoratorList.d.ts +43 -0
  58. package/dist/src/components/DecoratorList.d.ts.map +1 -0
  59. package/dist/src/components/DecoratorList.js +47 -0
  60. package/dist/src/components/DecoratorList.js.map +1 -0
  61. package/dist/src/components/EnumDeclaration.d.ts +9 -0
  62. package/dist/src/components/EnumDeclaration.d.ts.map +1 -1
  63. package/dist/src/components/EnumDeclaration.js +6 -1
  64. package/dist/src/components/EnumDeclaration.js.map +1 -1
  65. package/dist/src/components/FunctionBase.d.ts +31 -1
  66. package/dist/src/components/FunctionBase.d.ts.map +1 -1
  67. package/dist/src/components/FunctionBase.js +9 -2
  68. package/dist/src/components/FunctionBase.js.map +1 -1
  69. package/dist/src/components/FutureStatement.d.ts +1 -1
  70. package/dist/src/components/FutureStatement.js +1 -1
  71. package/dist/src/components/MethodBase.d.ts.map +1 -1
  72. package/dist/src/components/MethodBase.js +10 -2
  73. package/dist/src/components/MethodBase.js.map +1 -1
  74. package/dist/src/components/PropertyDeclaration.d.ts +29 -0
  75. package/dist/src/components/PropertyDeclaration.d.ts.map +1 -1
  76. package/dist/src/components/PropertyDeclaration.js +48 -1
  77. package/dist/src/components/PropertyDeclaration.js.map +1 -1
  78. package/dist/src/components/PydanticClassDeclaration.d.ts +120 -0
  79. package/dist/src/components/PydanticClassDeclaration.d.ts.map +1 -0
  80. package/dist/src/components/PydanticClassDeclaration.js +116 -0
  81. package/dist/src/components/PydanticClassDeclaration.js.map +1 -0
  82. package/dist/src/components/StaticMethodDeclaration.d.ts +3 -0
  83. package/dist/src/components/StaticMethodDeclaration.d.ts.map +1 -1
  84. package/dist/src/components/StaticMethodDeclaration.js +13 -3
  85. package/dist/src/components/StaticMethodDeclaration.js.map +1 -1
  86. package/dist/src/components/index.d.ts +1 -0
  87. package/dist/src/components/index.d.ts.map +1 -1
  88. package/dist/src/components/index.js +1 -0
  89. package/dist/src/components/index.js.map +1 -1
  90. package/dist/test/classdeclarations.test.js +25 -0
  91. package/dist/test/classdeclarations.test.js.map +1 -1
  92. package/dist/test/dataclassdeclarations.test.js +25 -0
  93. package/dist/test/dataclassdeclarations.test.js.map +1 -1
  94. package/dist/test/decoratorlist.test.d.ts +2 -0
  95. package/dist/test/decoratorlist.test.d.ts.map +1 -0
  96. package/dist/test/decoratorlist.test.js +60 -0
  97. package/dist/test/decoratorlist.test.js.map +1 -0
  98. package/dist/test/enums.test.js +27 -0
  99. package/dist/test/enums.test.js.map +1 -1
  100. package/dist/test/functiondeclaration.test.js +16 -0
  101. package/dist/test/functiondeclaration.test.js.map +1 -1
  102. package/dist/test/methoddeclaration.test.js +67 -0
  103. package/dist/test/methoddeclaration.test.js.map +1 -1
  104. package/dist/test/propertydeclaration.test.js +71 -1
  105. package/dist/test/propertydeclaration.test.js.map +1 -1
  106. package/dist/test/pydanticclassdeclarations.test.d.ts +2 -0
  107. package/dist/test/pydanticclassdeclarations.test.d.ts.map +1 -0
  108. package/dist/test/pydanticclassdeclarations.test.js +829 -0
  109. package/dist/test/pydanticclassdeclarations.test.js.map +1 -0
  110. package/dist/tsconfig.tsbuildinfo +1 -1
  111. package/docs/api/components/ClassDeclaration.md +10 -7
  112. package/docs/api/components/ClassEnumDeclaration.md +9 -6
  113. package/docs/api/components/ClassMethodDeclaration.md +7 -5
  114. package/docs/api/components/DataclassDeclaration.md +9 -5
  115. package/docs/api/components/DunderMethodDeclaration.md +7 -5
  116. package/docs/api/components/FunctionDeclaration.md +9 -5
  117. package/docs/api/components/FutureStatement.md +1 -1
  118. package/docs/api/components/MethodDeclaration.md +11 -6
  119. package/docs/api/components/PropertyDeclaration.md +11 -8
  120. package/docs/api/components/PydanticClassDeclaration.md +146 -0
  121. package/docs/api/components/StaticMethodDeclaration.md +7 -5
  122. package/docs/api/components/index.md +1 -0
  123. package/docs/api/index.md +3 -3
  124. package/docs/api/types/CommonFunctionProps.md +4 -3
  125. package/docs/api/types/PydanticModelConfigDictProps.md +32 -0
  126. package/docs/api/types/index.md +1 -0
  127. package/docs/api/variables/index.md +3 -0
  128. package/docs/api/variables/pydanticModule.md +27 -0
  129. package/docs/api/variables/pydanticSettingsModule.md +7 -0
  130. package/docs/api/variables/typingModule.md +9 -0
  131. package/package.json +4 -4
  132. package/src/builtins/python.ts +539 -1
  133. package/src/components/ClassDeclaration.tsx +23 -0
  134. package/src/components/ClassMethodDeclaration.tsx +9 -1
  135. package/src/components/DataclassDeclaration.tsx +18 -11
  136. package/src/components/DecoratorList.tsx +50 -0
  137. package/src/components/EnumDeclaration.tsx +11 -0
  138. package/src/components/FunctionBase.tsx +34 -3
  139. package/src/components/FutureStatement.tsx +1 -1
  140. package/src/components/MethodBase.tsx +6 -2
  141. package/src/components/PropertyDeclaration.tsx +48 -1
  142. package/src/components/PydanticClassDeclaration.tsx +222 -0
  143. package/src/components/StaticMethodDeclaration.tsx +7 -1
  144. package/src/components/index.ts +1 -0
  145. package/temp/api.json +1142 -84
  146. package/test/classdeclarations.test.tsx +27 -0
  147. package/test/dataclassdeclarations.test.tsx +25 -0
  148. package/test/decoratorlist.test.tsx +95 -0
  149. package/test/enums.test.tsx +29 -0
  150. package/test/functiondeclaration.test.tsx +17 -0
  151. package/test/methoddeclaration.test.tsx +70 -0
  152. package/test/propertydeclaration.test.tsx +66 -1
  153. package/test/pydanticclassdeclarations.test.tsx +836 -0
@@ -0,0 +1,836 @@
1
+ import { Prose, code, refkey } from "@alloy-js/core";
2
+ import { d } from "@alloy-js/core/testing";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ pydanticModule,
6
+ pydanticSettingsModule,
7
+ typingModule,
8
+ } from "../src/builtins/python.js";
9
+ import * as py from "../src/index.js";
10
+ import {
11
+ assertFileContents,
12
+ toSourceText,
13
+ toSourceTextMultiple,
14
+ } from "./utils.jsx";
15
+
16
+ describe("PydanticClassDeclaration", () => {
17
+ it("forwards class-level decorators above `class`", () => {
18
+ const res = toSourceText(
19
+ [
20
+ <py.SourceFile path="models.py">
21
+ <py.PydanticClassDeclaration
22
+ name="User"
23
+ decorators={[code`@${typingModule["."].final}`]}
24
+ >
25
+ <py.VariableDeclaration
26
+ instanceVariable
27
+ omitNone
28
+ name="id"
29
+ type="int"
30
+ />
31
+ </py.PydanticClassDeclaration>
32
+ </py.SourceFile>,
33
+ ],
34
+ { externals: [pydanticModule, typingModule] },
35
+ );
36
+
37
+ expect(res).toRenderTo(
38
+ d`
39
+ from pydantic import BaseModel
40
+ from typing import final
41
+
42
+
43
+ @final
44
+ class User(BaseModel):
45
+ id: int
46
+
47
+
48
+ `,
49
+ );
50
+ });
51
+
52
+ it("emits a pydantic model with BaseModel, fields, and Field import", () => {
53
+ const res = toSourceText(
54
+ [
55
+ <py.SourceFile path="models.py">
56
+ <py.PydanticClassDeclaration name="User">
57
+ <py.VariableDeclaration
58
+ instanceVariable
59
+ omitNone
60
+ name="id"
61
+ type="int"
62
+ />
63
+ <py.VariableDeclaration
64
+ instanceVariable
65
+ name="name"
66
+ type="str"
67
+ initializer={code`${pydanticModule["."].Field}(default="anon")`}
68
+ />
69
+ </py.PydanticClassDeclaration>
70
+ </py.SourceFile>,
71
+ ],
72
+ { externals: [pydanticModule] },
73
+ );
74
+
75
+ expect(res).toRenderTo(
76
+ d`
77
+ from pydantic import BaseModel
78
+ from pydantic import Field
79
+
80
+
81
+ class User(BaseModel):
82
+ id: int
83
+ name: str = Field(default="anon")
84
+
85
+
86
+ `,
87
+ );
88
+ });
89
+
90
+ it("inherits from another pydantic model via bases", () => {
91
+ const baseRef = refkey();
92
+ const res = toSourceText(
93
+ [
94
+ <py.SourceFile path="models.py">
95
+ <py.StatementList>
96
+ <py.PydanticClassDeclaration name="User" refkey={baseRef}>
97
+ <py.VariableDeclaration
98
+ instanceVariable
99
+ omitNone
100
+ name="id"
101
+ type="int"
102
+ />
103
+ </py.PydanticClassDeclaration>
104
+ <py.PydanticClassDeclaration name="Admin" bases={[baseRef]}>
105
+ <py.VariableDeclaration
106
+ instanceVariable
107
+ omitNone
108
+ name="role"
109
+ type="str"
110
+ />
111
+ </py.PydanticClassDeclaration>
112
+ </py.StatementList>
113
+ </py.SourceFile>,
114
+ ],
115
+ { externals: [pydanticModule] },
116
+ );
117
+
118
+ expect(res).toRenderTo(
119
+ d`
120
+ from pydantic import BaseModel
121
+
122
+
123
+ class User(BaseModel):
124
+ id: int
125
+
126
+ class Admin(User):
127
+ role: str
128
+
129
+
130
+ `,
131
+ );
132
+ });
133
+
134
+ it("supports required, optional, Field default, and plain default", () => {
135
+ const res = toSourceText(
136
+ [
137
+ <py.SourceFile path="models.py">
138
+ <py.PydanticClassDeclaration name="Item">
139
+ <py.VariableDeclaration
140
+ instanceVariable
141
+ omitNone
142
+ name="sku"
143
+ type="str"
144
+ />
145
+ <py.VariableDeclaration
146
+ instanceVariable
147
+ name="notes"
148
+ type={<py.UnionTypeExpression children={["str", "None"]} />}
149
+ />
150
+ <py.VariableDeclaration
151
+ instanceVariable
152
+ name="label"
153
+ type="str"
154
+ initializer={code`${pydanticModule["."].Field}(default="untitled")`}
155
+ />
156
+ <py.VariableDeclaration
157
+ instanceVariable
158
+ name="qty"
159
+ type="int"
160
+ initializer={1}
161
+ />
162
+ </py.PydanticClassDeclaration>
163
+ </py.SourceFile>,
164
+ ],
165
+ { externals: [pydanticModule] },
166
+ );
167
+
168
+ expect(res).toRenderTo(
169
+ d`
170
+ from pydantic import BaseModel
171
+ from pydantic import Field
172
+
173
+
174
+ class Item(BaseModel):
175
+ sku: str
176
+ notes: str | None = None
177
+ label: str = Field(default="untitled")
178
+ qty: int = 1
179
+
180
+
181
+ `,
182
+ );
183
+ });
184
+
185
+ it("emits class docstring", () => {
186
+ const doc = (
187
+ <py.ClassDoc description={[<Prose>Payload for an API request.</Prose>]} />
188
+ );
189
+ const res = toSourceText(
190
+ [
191
+ <py.SourceFile path="models.py">
192
+ <py.PydanticClassDeclaration name="RequestBody" doc={doc}>
193
+ <py.VariableDeclaration
194
+ instanceVariable
195
+ omitNone
196
+ name="value"
197
+ type="str"
198
+ />
199
+ </py.PydanticClassDeclaration>
200
+ </py.SourceFile>,
201
+ ],
202
+ { externals: [pydanticModule] },
203
+ );
204
+
205
+ expect(res).toRenderTo(
206
+ d`
207
+ from pydantic import BaseModel
208
+
209
+
210
+ class RequestBody(BaseModel):
211
+ """
212
+ Payload for an API request.
213
+ """
214
+
215
+ value: str
216
+
217
+
218
+ `,
219
+ );
220
+ });
221
+
222
+ it("resolves refkey across files with pydantic imports", () => {
223
+ const modelRef = refkey();
224
+ const res = toSourceTextMultiple(
225
+ [
226
+ <py.SourceFile path="models.py">
227
+ <py.PydanticClassDeclaration name="User" refkey={modelRef}>
228
+ <py.VariableDeclaration
229
+ instanceVariable
230
+ omitNone
231
+ name="id"
232
+ type="int"
233
+ />
234
+ </py.PydanticClassDeclaration>
235
+ </py.SourceFile>,
236
+ <py.SourceFile path="service.py">
237
+ <py.FunctionDeclaration
238
+ name="load_user"
239
+ returnType={modelRef}
240
+ parameters={[{ name: "user_id", type: "int" }]}
241
+ >
242
+ {"return User(id=user_id)"}
243
+ </py.FunctionDeclaration>
244
+ </py.SourceFile>,
245
+ ],
246
+ { externals: [pydanticModule] },
247
+ );
248
+
249
+ assertFileContents(res, {
250
+ "models.py": `
251
+ from pydantic import BaseModel
252
+
253
+
254
+ class User(BaseModel):
255
+ id: int
256
+
257
+ `,
258
+ "service.py": `
259
+ from typing import TYPE_CHECKING
260
+
261
+ if TYPE_CHECKING:
262
+ from models import User
263
+
264
+
265
+ def load_user(user_id: int) -> User:
266
+ return User(id=user_id)
267
+
268
+ `,
269
+ });
270
+ });
271
+
272
+ it("emits model_config = ConfigDict(...) from top-level config props", () => {
273
+ const res = toSourceText(
274
+ [
275
+ <py.SourceFile path="models.py">
276
+ <py.PydanticClassDeclaration
277
+ name="User"
278
+ frozen
279
+ extra="forbid"
280
+ validateAssignment
281
+ >
282
+ <py.VariableDeclaration
283
+ instanceVariable
284
+ omitNone
285
+ name="id"
286
+ type="int"
287
+ />
288
+ </py.PydanticClassDeclaration>
289
+ </py.SourceFile>,
290
+ ],
291
+ { externals: [pydanticModule] },
292
+ );
293
+
294
+ expect(res).toRenderTo(
295
+ d`
296
+ from pydantic import BaseModel
297
+ from pydantic import ConfigDict
298
+
299
+
300
+ class User(BaseModel):
301
+ model_config = ConfigDict(frozen=True, extra="forbid", validate_assignment=True)
302
+ id: int
303
+
304
+
305
+ `,
306
+ );
307
+ });
308
+
309
+ it("emits model_config = ConfigDict(...) from modelConfig", () => {
310
+ const res = toSourceText(
311
+ [
312
+ <py.SourceFile path="models.py">
313
+ <py.PydanticClassDeclaration
314
+ name="User"
315
+ modelConfig={{
316
+ frozen: true,
317
+ extra: "forbid",
318
+ validateAssignment: true,
319
+ }}
320
+ >
321
+ <py.VariableDeclaration
322
+ instanceVariable
323
+ omitNone
324
+ name="id"
325
+ type="int"
326
+ />
327
+ </py.PydanticClassDeclaration>
328
+ </py.SourceFile>,
329
+ ],
330
+ { externals: [pydanticModule] },
331
+ );
332
+
333
+ expect(res).toRenderTo(
334
+ d`
335
+ from pydantic import BaseModel
336
+ from pydantic import ConfigDict
337
+
338
+
339
+ class User(BaseModel):
340
+ model_config = ConfigDict(frozen=True, extra="forbid", validate_assignment=True)
341
+ id: int
342
+
343
+
344
+ `,
345
+ );
346
+ });
347
+
348
+ it("gives precedence to top-level config props over modelConfig", () => {
349
+ const res = toSourceText(
350
+ [
351
+ <py.SourceFile path="models.py">
352
+ <py.PydanticClassDeclaration
353
+ name="User"
354
+ modelConfig={{ frozen: false, extra: "allow" }}
355
+ frozen
356
+ extra="forbid"
357
+ />
358
+ </py.SourceFile>,
359
+ ],
360
+ { externals: [pydanticModule] },
361
+ );
362
+
363
+ expect(res).toRenderTo(
364
+ d`
365
+ from pydantic import BaseModel
366
+ from pydantic import ConfigDict
367
+
368
+
369
+ class User(BaseModel):
370
+ model_config = ConfigDict(frozen=True, extra="forbid")
371
+
372
+
373
+ `,
374
+ );
375
+ });
376
+
377
+ it("supports additional typed ConfigDict props", () => {
378
+ const res = toSourceText(
379
+ [
380
+ <py.SourceFile path="models.py">
381
+ <py.PydanticClassDeclaration
382
+ name="User"
383
+ useEnumValues
384
+ coerceNumbersToStr
385
+ validateReturn
386
+ strMinLength={1}
387
+ strMaxLength={128}
388
+ serJsonBytes="base64"
389
+ valJsonBytes="hex"
390
+ />
391
+ </py.SourceFile>,
392
+ ],
393
+ { externals: [pydanticModule] },
394
+ );
395
+
396
+ expect(res).toRenderTo(
397
+ d`
398
+ from pydantic import BaseModel
399
+ from pydantic import ConfigDict
400
+
401
+
402
+ class User(BaseModel):
403
+ model_config = ConfigDict(coerce_numbers_to_str=True, ser_json_bytes="base64", str_min_length=1, str_max_length=128, use_enum_values=True, val_json_bytes="hex", validate_return=True)
404
+
405
+
406
+ `,
407
+ );
408
+ });
409
+
410
+ it("imports SecretStr when used as a field type", () => {
411
+ const res = toSourceText(
412
+ [
413
+ <py.SourceFile path="models.py">
414
+ <py.PydanticClassDeclaration name="Credentials">
415
+ <py.VariableDeclaration
416
+ instanceVariable
417
+ omitNone
418
+ name="token"
419
+ type={pydanticModule["."].SecretStr}
420
+ />
421
+ </py.PydanticClassDeclaration>
422
+ </py.SourceFile>,
423
+ ],
424
+ { externals: [pydanticModule] },
425
+ );
426
+
427
+ expect(res).toRenderTo(
428
+ d`
429
+ from pydantic import BaseModel
430
+ from typing import TYPE_CHECKING
431
+
432
+ if TYPE_CHECKING:
433
+ from pydantic import SecretStr
434
+
435
+
436
+ class Credentials(BaseModel):
437
+ token: SecretStr
438
+
439
+
440
+ `,
441
+ );
442
+ });
443
+
444
+ it("emits arbitrary model_config via modelConfigExpression", () => {
445
+ const res = toSourceText(
446
+ [
447
+ <py.SourceFile path="models.py">
448
+ <py.PydanticClassDeclaration
449
+ name="M"
450
+ modelConfigExpression={code`${pydanticModule["."].ConfigDict}(frozen=True, extra="allow")`}
451
+ />
452
+ </py.SourceFile>,
453
+ ],
454
+ { externals: [pydanticModule] },
455
+ );
456
+
457
+ expect(res).toRenderTo(
458
+ d`
459
+ from pydantic import BaseModel
460
+ from pydantic import ConfigDict
461
+
462
+
463
+ class M(BaseModel):
464
+ model_config = ConfigDict(frozen=True, extra="allow")
465
+
466
+
467
+ `,
468
+ );
469
+ });
470
+
471
+ it("supports RootModel as explicit bases entry", () => {
472
+ const res = toSourceText(
473
+ [
474
+ <py.SourceFile path="models.py">
475
+ <py.PydanticClassDeclaration
476
+ name="Tags"
477
+ bases={[code`${pydanticModule["."].RootModel}[list[str]]`]}
478
+ />
479
+ </py.SourceFile>,
480
+ ],
481
+ { externals: [pydanticModule] },
482
+ );
483
+
484
+ expect(res).toRenderTo(
485
+ d`
486
+ from pydantic import RootModel
487
+
488
+
489
+ class Tags(RootModel[list[str]]):
490
+ pass
491
+
492
+
493
+ `,
494
+ );
495
+ });
496
+
497
+ it("places Pydantic validators above classmethod", () => {
498
+ const res = toSourceText(
499
+ [
500
+ <py.SourceFile path="models.py">
501
+ <py.PydanticClassDeclaration name="User">
502
+ <py.VariableDeclaration
503
+ instanceVariable
504
+ omitNone
505
+ name="name"
506
+ type="str"
507
+ />
508
+ <py.ClassMethodDeclaration
509
+ name="strip_name"
510
+ decorators={[
511
+ code`@${pydanticModule["."].field_validator}("name", mode="before")`,
512
+ ]}
513
+ parameters={[{ name: "value", type: "str" }]}
514
+ returnType="str"
515
+ >
516
+ {"return value.strip()"}
517
+ </py.ClassMethodDeclaration>
518
+ </py.PydanticClassDeclaration>
519
+ </py.SourceFile>,
520
+ ],
521
+ { externals: [pydanticModule] },
522
+ );
523
+
524
+ expect(res).toRenderTo(
525
+ d`
526
+ from pydantic import BaseModel
527
+ from pydantic import field_validator
528
+
529
+
530
+ class User(BaseModel):
531
+ name: str
532
+ @field_validator("name", mode="before")
533
+ @classmethod
534
+ def strip_name(cls, value: str) -> str:
535
+ return value.strip()
536
+
537
+
538
+
539
+ `,
540
+ );
541
+ });
542
+ });
543
+
544
+ describe("Pydantic ecosystem emitters", () => {
545
+ it("typing module resolves Any and similar annotations", () => {
546
+ const res = toSourceText(
547
+ [
548
+ <py.SourceFile path="models.py">
549
+ <py.FunctionDeclaration
550
+ name="identity"
551
+ parameters={[{ name: "x", type: typingModule["."].Any }]}
552
+ returnType={typingModule["."].Any}
553
+ >
554
+ {"return x"}
555
+ </py.FunctionDeclaration>
556
+ </py.SourceFile>,
557
+ ],
558
+ { externals: [typingModule] },
559
+ );
560
+
561
+ expect(res).toRenderTo(
562
+ d`
563
+ from typing import TYPE_CHECKING
564
+
565
+ if TYPE_CHECKING:
566
+ from typing import Any
567
+
568
+
569
+ def identity(x: Any) -> Any:
570
+ return x
571
+
572
+
573
+ `,
574
+ );
575
+ });
576
+
577
+ it("pydantic.types constrains field annotations", () => {
578
+ const res = toSourceText(
579
+ [
580
+ <py.SourceFile path="models.py">
581
+ <py.PydanticClassDeclaration name="Score">
582
+ <py.VariableDeclaration
583
+ instanceVariable
584
+ omitNone
585
+ name="points"
586
+ type={pydanticModule.types.PositiveInt}
587
+ />
588
+ </py.PydanticClassDeclaration>
589
+ </py.SourceFile>,
590
+ ],
591
+ { externals: [pydanticModule] },
592
+ );
593
+
594
+ expect(res).toRenderTo(
595
+ d`
596
+ from pydantic import BaseModel
597
+ from typing import TYPE_CHECKING
598
+
599
+ if TYPE_CHECKING:
600
+ from pydantic.types import PositiveInt
601
+
602
+
603
+ class Score(BaseModel):
604
+ points: PositiveInt
605
+
606
+
607
+ `,
608
+ );
609
+ });
610
+
611
+ it("postponed annotations support forward references in fields", () => {
612
+ const res = toSourceText(
613
+ [
614
+ <py.SourceFile
615
+ path="models.py"
616
+ futureImports={[<py.FutureStatement feature="annotations" />]}
617
+ >
618
+ <py.PydanticClassDeclaration name="Node">
619
+ <py.VariableDeclaration
620
+ instanceVariable
621
+ omitNone
622
+ name="label"
623
+ type="str"
624
+ />
625
+ <py.VariableDeclaration
626
+ instanceVariable
627
+ name="child"
628
+ type={code`"Node" | None`}
629
+ />
630
+ </py.PydanticClassDeclaration>
631
+ </py.SourceFile>,
632
+ ],
633
+ { externals: [pydanticModule] },
634
+ );
635
+
636
+ expect(res).toRenderTo(
637
+ d`
638
+ from __future__ import annotations
639
+
640
+ from pydantic import BaseModel
641
+
642
+
643
+ class Node(BaseModel):
644
+ label: str
645
+ child: "Node" | None = None
646
+
647
+
648
+ `,
649
+ );
650
+ });
651
+
652
+ it("model_config can use pydantic.alias_generators", () => {
653
+ const res = toSourceText(
654
+ [
655
+ <py.SourceFile path="models.py">
656
+ <py.PydanticClassDeclaration
657
+ name="M"
658
+ modelConfigExpression={code`${pydanticModule["."].ConfigDict}(alias_generator=${pydanticModule.alias_generators.to_camel})`}
659
+ />
660
+ </py.SourceFile>,
661
+ ],
662
+ { externals: [pydanticModule] },
663
+ );
664
+
665
+ expect(res).toRenderTo(
666
+ d`
667
+ from pydantic import BaseModel
668
+ from pydantic import ConfigDict
669
+ from pydantic.alias_generators import to_camel
670
+
671
+
672
+ class M(BaseModel):
673
+ model_config = ConfigDict(alias_generator=to_camel)
674
+
675
+
676
+ `,
677
+ );
678
+ });
679
+
680
+ it("pydantic_settings exposes BaseSettings", () => {
681
+ const res = toSourceText(
682
+ [
683
+ <py.SourceFile path="config.py">
684
+ <py.ClassDeclaration
685
+ name="AppSettings"
686
+ bases={[pydanticSettingsModule["."].BaseSettings]}
687
+ />
688
+ </py.SourceFile>,
689
+ ],
690
+ { externals: [pydanticSettingsModule] },
691
+ );
692
+
693
+ expect(res).toRenderTo(
694
+ d`
695
+ from pydantic_settings import BaseSettings
696
+
697
+
698
+ class AppSettings(BaseSettings):
699
+ pass
700
+
701
+
702
+ `,
703
+ );
704
+ });
705
+
706
+ it("emits @computed_field above @property via PropertyDeclaration", () => {
707
+ const res = toSourceText(
708
+ [
709
+ <py.SourceFile path="models.py">
710
+ <py.PydanticClassDeclaration name="Square">
711
+ <py.VariableDeclaration
712
+ instanceVariable
713
+ omitNone
714
+ name="width"
715
+ type="float"
716
+ />
717
+ <py.PropertyDeclaration
718
+ name="area"
719
+ type="float"
720
+ decorators={[code`@${pydanticModule["."].computed_field}`]}
721
+ >
722
+ {"return self.width ** 2"}
723
+ </py.PropertyDeclaration>
724
+ </py.PydanticClassDeclaration>
725
+ </py.SourceFile>,
726
+ ],
727
+ { externals: [pydanticModule] },
728
+ );
729
+
730
+ expect(res).toRenderTo(
731
+ d`
732
+ from pydantic import BaseModel
733
+ from pydantic import computed_field
734
+
735
+
736
+ class Square(BaseModel):
737
+ width: float
738
+ @computed_field
739
+ @property
740
+ def area(self) -> float:
741
+ return self.width ** 2
742
+
743
+
744
+
745
+ `,
746
+ );
747
+ });
748
+
749
+ it("emits computed_field on an instance method", () => {
750
+ const res = toSourceText(
751
+ [
752
+ <py.SourceFile path="models.py">
753
+ <py.PydanticClassDeclaration name="Square">
754
+ <py.VariableDeclaration
755
+ instanceVariable
756
+ omitNone
757
+ name="width"
758
+ type="float"
759
+ />
760
+ <py.MethodDeclaration
761
+ name="area"
762
+ decorators={[code`@${pydanticModule["."].computed_field}`]}
763
+ returnType="float"
764
+ >
765
+ {"return self.width ** 2"}
766
+ </py.MethodDeclaration>
767
+ </py.PydanticClassDeclaration>
768
+ </py.SourceFile>,
769
+ ],
770
+ { externals: [pydanticModule] },
771
+ );
772
+
773
+ expect(res).toRenderTo(
774
+ d`
775
+ from pydantic import BaseModel
776
+ from pydantic import computed_field
777
+
778
+
779
+ class Square(BaseModel):
780
+ width: float
781
+ @computed_field
782
+ def area(self) -> float:
783
+ return self.width ** 2
784
+
785
+
786
+
787
+ `,
788
+ );
789
+ });
790
+
791
+ it("emits model_validator above classmethod", () => {
792
+ const res = toSourceText(
793
+ [
794
+ <py.SourceFile path="models.py">
795
+ <py.PydanticClassDeclaration name="Bag">
796
+ <py.VariableDeclaration
797
+ instanceVariable
798
+ omitNone
799
+ name="items"
800
+ type="list"
801
+ />
802
+ <py.ClassMethodDeclaration
803
+ name="ensure_items"
804
+ decorators={[
805
+ code`@${pydanticModule["."].model_validator}(mode="before")`,
806
+ ]}
807
+ parameters={[{ name: "data", type: "dict" }]}
808
+ returnType="dict"
809
+ >
810
+ {"return data"}
811
+ </py.ClassMethodDeclaration>
812
+ </py.PydanticClassDeclaration>
813
+ </py.SourceFile>,
814
+ ],
815
+ { externals: [pydanticModule] },
816
+ );
817
+
818
+ expect(res).toRenderTo(
819
+ d`
820
+ from pydantic import BaseModel
821
+ from pydantic import model_validator
822
+
823
+
824
+ class Bag(BaseModel):
825
+ items: list
826
+ @model_validator(mode="before")
827
+ @classmethod
828
+ def ensure_items(cls, data: dict) -> dict:
829
+ return data
830
+
831
+
832
+
833
+ `,
834
+ );
835
+ });
836
+ });