lean-python 0.1.0__py3-none-any.whl

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.
lean_python/marshal.py ADDED
@@ -0,0 +1,727 @@
1
+ """Conversion between Python values and the runtime's Lean values, and the pydantic types of Lean types.
2
+
3
+ Generated modules describe each public type with a descriptor (``NAT``, ``ListD(...)``, ``NamedD``)
4
+ and each structure and constructor with a ``CtorInfo``. Values going to Lean are checked against
5
+ the Lean type (ranges, invariants) while they are converted; values coming back are built with
6
+ ``model_construct``, since a Lean value already satisfies its invariants.
7
+ """
8
+
9
+ import enum
10
+ import json
11
+ from typing import Annotated, Any, ClassVar
12
+
13
+ import pydantic
14
+ from pydantic import AfterValidator, BaseModel, ConfigDict, GetCoreSchemaHandler, GetJsonSchemaHandler
15
+ from pydantic_core import PydanticCustomError, SchemaSerializer, SchemaValidator, ValidationError, core_schema
16
+
17
+ from lean_python.runtime import Ctor, LArray, _f32, _list_to_py, _py_to_list
18
+
19
+ Field = pydantic.Field
20
+ """Used by generated models for the field options (``alias``, ``exclude_if``)."""
21
+
22
+
23
+ class LeanIOError(OSError):
24
+ """An `IO.Error` raised by a Lean `IO` action."""
25
+
26
+
27
+ class _Fail(Exception):
28
+ def __init__(self, msg, value, loc=()):
29
+ super().__init__(msg)
30
+ self.msg = msg
31
+ self.value = value
32
+ self.loc = loc
33
+
34
+
35
+ def _fail(msg, value):
36
+ raise _Fail(msg, value)
37
+
38
+
39
+ def _validation_error(title, f):
40
+ return ValidationError.from_exception_data(
41
+ title,
42
+ [{"type": PydanticCustomError("lean_type", f.msg), "loc": f.loc, "input": f.value}],
43
+ )
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Descriptors
48
+
49
+
50
+ class Desc:
51
+ def to_lean(self, v):
52
+ raise NotImplementedError
53
+
54
+ def to_py(self, x):
55
+ raise NotImplementedError
56
+
57
+
58
+ def _is_int(v):
59
+ return isinstance(v, int) and not isinstance(v, bool)
60
+
61
+
62
+ class _Nat(Desc):
63
+ def to_lean(self, v):
64
+ if not _is_int(v):
65
+ _fail("expected an int (Lean `Nat`)", v)
66
+ if v < 0:
67
+ _fail("expected a non-negative int (Lean `Nat`)", v)
68
+ return v
69
+
70
+ def to_py(self, x):
71
+ return x
72
+
73
+
74
+ class _Int(Desc):
75
+ def to_lean(self, v):
76
+ if not _is_int(v):
77
+ _fail("expected an int (Lean `Int`)", v)
78
+ return v
79
+
80
+ def to_py(self, x):
81
+ return x
82
+
83
+
84
+ class UIntD(Desc):
85
+ def __init__(self, bits):
86
+ self.bits = bits
87
+ self.limit = 1 << bits
88
+
89
+ def to_lean(self, v):
90
+ if not _is_int(v) or not 0 <= v < self.limit:
91
+ _fail(f"expected an int in [0, 2^{self.bits}) (Lean `UInt{self.bits}`)", v)
92
+ return v
93
+
94
+ def to_py(self, x):
95
+ return x
96
+
97
+
98
+ class SIntD(Desc):
99
+ def __init__(self, bits):
100
+ self.bits = bits
101
+ self.half = 1 << (bits - 1)
102
+ self.mask = (1 << bits) - 1
103
+
104
+ def to_lean(self, v):
105
+ if not _is_int(v) or not -self.half <= v < self.half:
106
+ _fail(f"expected an int in [-2^{self.bits - 1}, 2^{self.bits - 1}) (Lean `Int{self.bits}`)", v)
107
+ return v & self.mask
108
+
109
+ def to_py(self, x):
110
+ return x - (1 << self.bits) if x >= self.half else x
111
+
112
+
113
+ class FinD(Desc):
114
+ def __init__(self, n):
115
+ self.n = n
116
+
117
+ def to_lean(self, v):
118
+ if not _is_int(v) or not 0 <= v < self.n:
119
+ _fail(f"expected an int in [0, {self.n}) (Lean `Fin {self.n}`)", v)
120
+ return v
121
+
122
+ def to_py(self, x):
123
+ return x
124
+
125
+
126
+ class _Bool(Desc):
127
+ def to_lean(self, v):
128
+ if not isinstance(v, bool):
129
+ _fail("expected a bool", v)
130
+ return 1 if v else 0
131
+
132
+ def to_py(self, x):
133
+ return bool(x)
134
+
135
+
136
+ class _Char(Desc):
137
+ def to_lean(self, v):
138
+ if not isinstance(v, str) or len(v) != 1:
139
+ _fail("expected a str of one character (Lean `Char`)", v)
140
+ c = ord(v)
141
+ if 0xD800 <= c <= 0xDFFF:
142
+ _fail("a surrogate is not a Lean `Char`", v)
143
+ return c
144
+
145
+ def to_py(self, x):
146
+ return chr(x)
147
+
148
+
149
+ class _String(Desc):
150
+ def to_lean(self, v):
151
+ if not isinstance(v, str):
152
+ _fail("expected a str", v)
153
+ try:
154
+ return v.encode("utf-8")
155
+ except UnicodeEncodeError:
156
+ _fail("a str with a surrogate is not a Lean `String`", v)
157
+
158
+ def to_py(self, x):
159
+ return x.decode("utf-8")
160
+
161
+
162
+ class _Float(Desc):
163
+ def to_lean(self, v):
164
+ if isinstance(v, bool) or not isinstance(v, (int, float)):
165
+ _fail("expected a float", v)
166
+ return float(v)
167
+
168
+ def to_py(self, x):
169
+ return x
170
+
171
+
172
+ class _Float32(_Float):
173
+ def to_lean(self, v):
174
+ return _f32(super().to_lean(v))
175
+
176
+
177
+ class _Unit(Desc):
178
+ def to_lean(self, v):
179
+ if v is not None:
180
+ _fail("expected None (Lean `Unit`)", v)
181
+ return 0
182
+
183
+ def to_py(self, x):
184
+ return None
185
+
186
+
187
+ def _at(i, f):
188
+ f.loc = (i, *f.loc)
189
+ return f
190
+
191
+
192
+ class ListD(Desc):
193
+ def __init__(self, d):
194
+ self.d = d
195
+
196
+ def _items(self, v):
197
+ if not isinstance(v, (list, tuple)):
198
+ _fail("expected a list", v)
199
+ out = []
200
+ for i, x in enumerate(v):
201
+ try:
202
+ out.append(self.d.to_lean(x))
203
+ except _Fail as f:
204
+ raise _at(i, f) from None
205
+ return out
206
+
207
+ def to_lean(self, v):
208
+ return _py_to_list(self._items(v))
209
+
210
+ def to_py(self, x):
211
+ return [self.d.to_py(e) for e in _list_to_py(x)]
212
+
213
+
214
+ class ArrayD(ListD):
215
+ def to_lean(self, v):
216
+ return LArray(self._items(v))
217
+
218
+ def to_py(self, x):
219
+ return [self.d.to_py(e) for e in x.data]
220
+
221
+
222
+ class OptionD(Desc):
223
+ def __init__(self, d):
224
+ self.d = d
225
+
226
+ def to_lean(self, v):
227
+ if v is None:
228
+ return 0
229
+ return Ctor(1, [self.d.to_lean(v)])
230
+
231
+ def to_py(self, x):
232
+ if isinstance(x, int):
233
+ return None
234
+ return self.d.to_py(x.objs[0])
235
+
236
+
237
+ class ProdD(Desc):
238
+ def __init__(self, a, b):
239
+ self.a = a
240
+ self.b = b
241
+
242
+ def to_lean(self, v):
243
+ if not isinstance(v, (tuple, list)) or len(v) != 2:
244
+ _fail("expected a pair", v)
245
+ try:
246
+ a = self.a.to_lean(v[0])
247
+ except _Fail as f:
248
+ raise _at(0, f) from None
249
+ try:
250
+ b = self.b.to_lean(v[1])
251
+ except _Fail as f:
252
+ raise _at(1, f) from None
253
+ return Ctor(0, [a, b])
254
+
255
+ def to_py(self, x):
256
+ return (self.a.to_py(x.objs[0]), self.b.to_py(x.objs[1]))
257
+
258
+
259
+ class NamedD(Desc):
260
+ """A generated type, looked up by name in the generated module (types may be recursive)."""
261
+
262
+ def __init__(self, g, name):
263
+ self.g = g
264
+ self.name = name
265
+ self._info = None
266
+
267
+ def info(self):
268
+ if self._info is None:
269
+ first, *rest = self.name.split(".")
270
+ t = self.g[first]
271
+ for r in rest:
272
+ t = getattr(t, r)
273
+ meta = getattr(t, "__metadata__", ())
274
+ infos = [m for m in meta if isinstance(m, SubtypeInfo)]
275
+ self._info = infos[0] if infos else t.__lean__
276
+ return self._info
277
+
278
+ def to_lean(self, v):
279
+ return self.info().to_lean(v)
280
+
281
+ def to_py(self, x):
282
+ return self.info().to_py(x)
283
+
284
+
285
+ NAT = _Nat()
286
+ INT = _Int()
287
+ BOOL = _Bool()
288
+ CHAR = _Char()
289
+ STRING = _String()
290
+ FLOAT = _Float()
291
+ FLOAT32 = _Float32()
292
+ UNIT = _Unit()
293
+
294
+
295
+ def to_lean(desc, v, name):
296
+ try:
297
+ return desc.to_lean(v)
298
+ except _Fail as f:
299
+ f.loc = (name, *f.loc)
300
+ raise _validation_error("lean-python", f) from None
301
+
302
+
303
+ def to_py(desc, x):
304
+ return desc.to_py(x)
305
+
306
+
307
+ def io_result(r, desc, error_to_string):
308
+ if r.tag == 0:
309
+ return desc.to_py(r.objs[0])
310
+ raise LeanIOError(error_to_string(r.objs[0]).decode("utf-8"))
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # Structures and constructors
315
+
316
+
317
+ class Obj:
318
+ __slots__ = ("i",)
319
+
320
+ def __init__(self, i):
321
+ self.i = i
322
+
323
+
324
+ class Scalar:
325
+ __slots__ = ("key",)
326
+
327
+ def __init__(self, key):
328
+ self.key = key
329
+
330
+
331
+ ERASED = None
332
+
333
+
334
+ class FieldInfo:
335
+ __slots__ = ("desc", "loc", "name")
336
+
337
+ def __init__(self, name, loc, desc):
338
+ self.name = name
339
+ self.loc = loc
340
+ self.desc = desc
341
+
342
+
343
+ class Invariant:
344
+ __slots__ = ("args", "field", "fn", "prop")
345
+
346
+ def __init__(self, field, prop, fn, args):
347
+ self.field = field
348
+ self.prop = prop
349
+ self.fn = fn
350
+ self.args = args
351
+
352
+ def holds(self, values):
353
+ return bool(self.fn(*[values[i] for i in self.args]))
354
+
355
+
356
+ class CtorInfo:
357
+ def __init__(self, name, cidx, size, unit, fields, invariants, positional):
358
+ self.name = name
359
+ self.cidx = cidx
360
+ self.size = size
361
+ self.unit = unit
362
+ self.fields = fields
363
+ self.invariants = invariants
364
+ self.positional = positional
365
+
366
+ def lean_values(self, v):
367
+ values = []
368
+ for f in self.fields:
369
+ try:
370
+ values.append(f.desc.to_lean(getattr(v, f.name)))
371
+ except _Fail as e:
372
+ raise _at(f.name, e) from None
373
+ for inv in self.invariants:
374
+ if not inv.holds(values):
375
+ _fail(f"{inv.field}: {inv.prop} does not hold", v)
376
+ return values
377
+
378
+ def build(self, values):
379
+ if self.unit:
380
+ return self.cidx
381
+ objs = [0] * self.size
382
+ o = Ctor(self.cidx, objs)
383
+ for f, x in zip(self.fields, values, strict=True):
384
+ loc = f.loc
385
+ if loc.__class__ is Obj:
386
+ objs[loc.i] = x
387
+ elif loc.__class__ is Scalar:
388
+ o.sc[loc.key] = x
389
+ return o
390
+
391
+ def read(self, cls, x):
392
+ kw = {}
393
+ for f in self.fields:
394
+ loc = f.loc
395
+ if loc.__class__ is Obj:
396
+ kw[f.name] = f.desc.to_py(x.objs[loc.i])
397
+ elif loc.__class__ is Scalar:
398
+ kw[f.name] = f.desc.to_py(x.sc[loc.key])
399
+ return cls.model_construct(**kw)
400
+
401
+
402
+ class StructInfo:
403
+ def __init__(self, cls, ctor, trivial=False):
404
+ self.cls = cls
405
+ self.ctor = ctor
406
+ self.trivial = trivial
407
+ cls.__lean_ctor__ = ctor
408
+
409
+ def to_lean(self, v):
410
+ if not isinstance(v, self.cls):
411
+ try:
412
+ v = self.cls.model_validate(v)
413
+ except ValidationError as e:
414
+ _fail(str(e), v)
415
+ values = self.ctor.lean_values(v)
416
+ if self.trivial:
417
+ return values[0]
418
+ return self.ctor.build(values)
419
+
420
+ def to_py(self, x):
421
+ if self.trivial:
422
+ f = self.ctor.fields[0]
423
+ return self.cls.model_construct(**{f.name: f.desc.to_py(x)})
424
+ return self.ctor.read(self.cls, x)
425
+
426
+
427
+ class SubtypeInfo:
428
+ def __init__(self, desc, invariant):
429
+ self.desc = desc
430
+ self.invariant = invariant
431
+
432
+ def to_lean(self, v):
433
+ x = self.desc.to_lean(v)
434
+ if not self.invariant.holds([x]):
435
+ _fail(f"{self.invariant.prop} does not hold", v)
436
+ return x
437
+
438
+ def to_py(self, x):
439
+ return self.desc.to_py(x)
440
+
441
+ def __call__(self, v):
442
+ try:
443
+ self.to_lean(v)
444
+ except _Fail as f:
445
+ raise ValueError(f.msg) from None
446
+ return v
447
+
448
+
449
+ def adopt(cls, ctor, trivial=False):
450
+ """Attaches the layout of the Lean structure made from the user's pydantic model ``cls``."""
451
+ cls.__lean__ = StructInfo(cls, ctor, trivial)
452
+
453
+
454
+ def subtype(pytype, desc, invariant, annotations=()):
455
+ info = SubtypeInfo(desc, invariant)
456
+ return Annotated[pytype, *annotations, AfterValidator(info), info]
457
+
458
+
459
+ def is_none(v):
460
+ return v is None
461
+
462
+
463
+ class LeanModel(BaseModel):
464
+ """A Lean structure or constructor: frozen, strict, no extra fields."""
465
+
466
+ model_config = ConfigDict(
467
+ frozen=True,
468
+ strict=True,
469
+ extra="forbid",
470
+ validate_by_name=True,
471
+ validate_by_alias=True,
472
+ serialize_by_alias=True,
473
+ )
474
+
475
+ @pydantic.model_validator(mode="after")
476
+ def _lean_invariants(self):
477
+ ctor = type(self).__dict__.get("__lean_ctor__")
478
+ if ctor is not None and ctor.invariants:
479
+ try:
480
+ ctor.lean_values(self)
481
+ except _Fail as f:
482
+ raise ValueError(f.msg) from None
483
+ return self
484
+
485
+
486
+ class LeanEnum(enum.StrEnum):
487
+ """A Lean inductive type whose constructors take no arguments."""
488
+
489
+
490
+ class EnumInfo:
491
+ def __init__(self, cls, plain=False):
492
+ self.cls = cls
493
+ self.plain = plain
494
+ self.members = list(cls)
495
+ self.index = {m: i for i, m in enumerate(self.members)}
496
+
497
+ def to_lean(self, v):
498
+ if not isinstance(v, self.cls):
499
+ try:
500
+ v = self.cls(v)
501
+ except ValueError:
502
+ _fail(f"expected a {self.cls.__name__}", v)
503
+ return self.index[v]
504
+
505
+ def to_py(self, x):
506
+ m = self.members[x]
507
+ return m.value if self.plain else m
508
+
509
+
510
+ def adopt_enum(cls):
511
+ """Attaches the Lean inductive type made from the user's enum ``cls`` (a constructor per member)."""
512
+ cls.__lean__ = EnumInfo(cls)
513
+
514
+
515
+ class ModelUnion:
516
+ """A union of the user's models (``Cat | Dog``): a Lean inductive type with a constructor per model."""
517
+
518
+ def __init__(self, alternatives):
519
+ self.alternatives = alternatives
520
+ self.__lean__ = self
521
+
522
+ def to_lean(self, v):
523
+ for cidx, d in self.alternatives:
524
+ if isinstance(v, d.info().cls):
525
+ return Ctor(cidx, [d.to_lean(v)])
526
+ _fail("expected one of " + ", ".join(d.info().cls.__name__ for _, d in self.alternatives), v)
527
+
528
+ def to_py(self, x):
529
+ for cidx, d in self.alternatives:
530
+ if cidx == x.tag:
531
+ return d.to_py(x.objs[0])
532
+ raise AssertionError(x.tag)
533
+
534
+
535
+ # ---------------------------------------------------------------------------
536
+ # Inductive types: a model per constructor, JSON as Lean's `deriving ToJson, FromJson`
537
+
538
+
539
+ class LeanInductive(BaseModel):
540
+ """Base class of the constructor models of a Lean inductive type.
541
+
542
+ As a pydantic type it accepts an instance of a constructor model or Lean's JSON encoding:
543
+ ``"ctor"`` for a constructor without arguments, ``{"ctor": {...}}`` for named arguments,
544
+ ``{"ctor": value}`` or ``{"ctor": [v1, v2]}`` for unnamed ones.
545
+ """
546
+
547
+ model_config = ConfigDict(frozen=True, strict=True, extra="forbid")
548
+
549
+ __lean_ctors__: ClassVar[dict] = {}
550
+
551
+ @classmethod
552
+ def _lean_payload(cls, sub, payload):
553
+ info = sub.__lean_ctor__
554
+ names = [f.name for f in info.fields]
555
+ if info.positional and names:
556
+ if len(names) == 1:
557
+ return {names[0]: payload}
558
+ if not isinstance(payload, (list, tuple)) or len(payload) != len(names):
559
+ raise PydanticCustomError("lean_tag", "expected {n} arguments", {"n": len(names)})
560
+ return dict(zip(names, payload, strict=True))
561
+ if not names and payload in (None, {}):
562
+ return {}
563
+ return payload
564
+
565
+ @classmethod
566
+ def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler):
567
+ if "__lean_ctors__" not in cls.__dict__:
568
+ return handler(source)
569
+ ctors = cls.__lean_ctors__
570
+
571
+ def validate(v, info):
572
+ if isinstance(v, cls):
573
+ return v
574
+ if isinstance(v, str):
575
+ tag, payload = v, None
576
+ elif isinstance(v, dict) and len(v) == 1:
577
+ ((tag, payload),) = v.items()
578
+ else:
579
+ raise PydanticCustomError(
580
+ "lean_tag", "expected a constructor of {t}: a name or an object with one key", {"t": cls.__name__}
581
+ )
582
+ sub = ctors.get(tag)
583
+ if sub is None:
584
+ raise PydanticCustomError(
585
+ "lean_tag", "unknown constructor {tag} of {t}", {"tag": tag, "t": cls.__name__}
586
+ )
587
+ data = cls._lean_payload(sub, payload)
588
+ if info.mode == "json":
589
+ return sub.model_validate_json(json.dumps(data))
590
+ return sub.model_validate(data)
591
+
592
+ def serialize(v, info):
593
+ sub = type(v)
594
+ ci = sub.__lean_ctor__
595
+ if ci.unit or not ci.fields:
596
+ return ci.name
597
+ d = v.model_dump(mode="json" if info.mode == "json" else "python")
598
+ if ci.positional:
599
+ vals = [d[f.name] for f in ci.fields]
600
+ return {ci.name: vals[0] if len(vals) == 1 else vals}
601
+ return {ci.name: d}
602
+
603
+ def json_schema(_schema, handler):
604
+ return cls._lean_json_schema(handler)
605
+
606
+ return core_schema.with_info_plain_validator_function(
607
+ validate,
608
+ serialization=core_schema.plain_serializer_function_ser_schema(serialize, info_arg=True),
609
+ metadata={"pydantic_js_functions": [json_schema]},
610
+ )
611
+
612
+ @classmethod
613
+ def __get_pydantic_json_schema__(cls, schema, handler: GetJsonSchemaHandler):
614
+ if "__lean_ctors__" not in cls.__dict__:
615
+ return super().__get_pydantic_json_schema__(schema, handler)
616
+ return cls._lean_json_schema(handler)
617
+
618
+ @classmethod
619
+ def _lean_json_schema(cls, handler: GetJsonSchemaHandler):
620
+ variants = []
621
+ for tag, sub in cls.__lean_ctors__.items():
622
+ ci = sub.__lean_ctor__
623
+ if ci.unit or not ci.fields:
624
+ variants.append({"const": tag})
625
+ continue
626
+ payload = _inline_defs(sub.model_json_schema(mode=getattr(handler, "mode", "validation")))
627
+ if ci.positional:
628
+ props = payload.get("properties", {})
629
+ items = [props.get(f.name, {}) for f in ci.fields]
630
+ payload = (
631
+ items[0]
632
+ if len(items) == 1
633
+ else {"type": "array", "prefixItems": items, "minItems": len(items), "maxItems": len(items)}
634
+ )
635
+ variants.append(
636
+ {"type": "object", "properties": {tag: payload}, "required": [tag], "additionalProperties": False}
637
+ )
638
+ return {"oneOf": variants, "title": cls.__name__, "description": cls.__doc__}
639
+
640
+
641
+ def _inline_defs(schema):
642
+ """The JSON Schema with its ``$defs`` inlined (a recursive reference stays a ``$ref``)."""
643
+ defs = schema.pop("$defs", {})
644
+
645
+ def go(node, seen):
646
+ if isinstance(node, dict):
647
+ ref = node.get("$ref")
648
+ if isinstance(ref, str) and ref.startswith("#/$defs/"):
649
+ name = ref.removeprefix("#/$defs/")
650
+ if name in seen or name not in defs:
651
+ return node
652
+ return go(defs[name], seen | {name})
653
+ return {k: go(v, seen) for k, v in node.items()}
654
+ if isinstance(node, list):
655
+ return [go(v, seen) for v in node]
656
+ return node
657
+
658
+ return go(schema, frozenset())
659
+
660
+
661
+ class InductiveInfo:
662
+ def __init__(self, base, pairs):
663
+ self.base = base
664
+ self.by_tag = {}
665
+ for sub, ci in pairs:
666
+ self.by_tag[ci.cidx] = (sub, ci)
667
+
668
+ def to_lean(self, v):
669
+ if not isinstance(v, self.base):
670
+ try:
671
+ v = pydantic.TypeAdapter(self.base).validate_python(v)
672
+ except ValidationError as e:
673
+ _fail(str(e), v)
674
+ ci = type(v).__lean_ctor__
675
+ return ci.build(ci.lean_values(v))
676
+
677
+ def to_py(self, x):
678
+ tag = x if isinstance(x, int) else x.tag
679
+ sub, ci = self.by_tag[tag]
680
+ if ci.unit:
681
+ return sub.model_construct()
682
+ return ci.read(sub, x)
683
+
684
+
685
+ def register_inductive(base, pairs):
686
+ ctors = {}
687
+ for sub, ci in pairs:
688
+ sub.__lean_ctor__ = ci
689
+ sub.__name__ = ci.name
690
+ setattr(base, ci.name, sub)
691
+ ctors[ci.name] = sub
692
+ base.__lean_ctors__ = ctors
693
+ base.__lean__ = InductiveInfo(base, pairs)
694
+ schema = base.__get_pydantic_core_schema__(base, None)
695
+ base.__pydantic_core_schema__ = schema
696
+ base.__pydantic_validator__ = SchemaValidator(schema)
697
+ base.__pydantic_serializer__ = SchemaSerializer(schema)
698
+
699
+
700
+ # ---------------------------------------------------------------------------
701
+ # Module assembly
702
+
703
+
704
+ def attach(g, path, fn):
705
+ fn.__name__ = path[-1]
706
+ fn.__qualname__ = ".".join(path)
707
+ if len(path) == 1:
708
+ g[path[0]] = fn
709
+ return
710
+ holder = g.get(path[0])
711
+ if holder is None:
712
+ holder = type(path[0], (), {"__module__": g.get("__name__")})
713
+ g[path[0]] = holder
714
+ for p in path[1:-1]:
715
+ nxt = getattr(holder, p, None)
716
+ if nxt is None:
717
+ nxt = type(p, (), {"__module__": g.get("__name__")})
718
+ setattr(holder, p, nxt)
719
+ holder = nxt
720
+ setattr(holder, path[-1], staticmethod(fn))
721
+
722
+
723
+ def finish(g):
724
+ for v in list(g.values()):
725
+ if isinstance(v, type) and issubclass(v, LeanModel) and v is not LeanModel:
726
+ v.model_rebuild(force=True, _types_namespace=g)
727
+ v.__match_args__ = tuple(v.model_fields)