velarium 0.1.0__tar.gz
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.
- velarium-0.1.0/.gitignore +61 -0
- velarium-0.1.0/LICENSE +21 -0
- velarium-0.1.0/PKG-INFO +58 -0
- velarium-0.1.0/README.md +32 -0
- velarium-0.1.0/pyproject.toml +45 -0
- velarium-0.1.0/velarium/__init__.py +46 -0
- velarium-0.1.0/velarium/annotations.py +200 -0
- velarium-0.1.0/velarium/ir.py +86 -0
- velarium-0.1.0/velarium/json_codec.py +156 -0
- velarium-0.1.0/velarium/modelspec_build.py +101 -0
- velarium-0.1.0/velarium/normalize.py +98 -0
- velarium-0.1.0/velarium/py.typed +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Byte-compiled / optimized
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Virtual environments
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
ENV/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Packaging / build
|
|
14
|
+
build/
|
|
15
|
+
dist/
|
|
16
|
+
packages/*/dist/
|
|
17
|
+
*.egg-info/
|
|
18
|
+
*.egg
|
|
19
|
+
.eggs/
|
|
20
|
+
pip-wheel-metadata/
|
|
21
|
+
share/python-wheels/
|
|
22
|
+
|
|
23
|
+
# Hatch / setuptools
|
|
24
|
+
.hatch/
|
|
25
|
+
|
|
26
|
+
# Testing / coverage
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.coverage
|
|
29
|
+
.coverage.*
|
|
30
|
+
htmlcov/
|
|
31
|
+
.tox/
|
|
32
|
+
.nox/
|
|
33
|
+
coverage.xml
|
|
34
|
+
*.cover
|
|
35
|
+
.hypothesis/
|
|
36
|
+
|
|
37
|
+
# Type checkers / linters
|
|
38
|
+
.mypy_cache/
|
|
39
|
+
.dmypy.json
|
|
40
|
+
dmypy.json
|
|
41
|
+
.ruff_cache/
|
|
42
|
+
.pytype/
|
|
43
|
+
|
|
44
|
+
# IDEs / editors
|
|
45
|
+
.idea/
|
|
46
|
+
.vscode/
|
|
47
|
+
*.swp
|
|
48
|
+
*.swo
|
|
49
|
+
*~
|
|
50
|
+
|
|
51
|
+
# OS
|
|
52
|
+
.DS_Store
|
|
53
|
+
Thumbs.db
|
|
54
|
+
|
|
55
|
+
# Local env / secrets
|
|
56
|
+
.env
|
|
57
|
+
.env.*
|
|
58
|
+
!.env.example
|
|
59
|
+
|
|
60
|
+
# Cursor / tooling (optional local state)
|
|
61
|
+
*.log
|
velarium-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) stubber contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
velarium-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: velarium
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Velarium IR (ModelSpec): normalization, JSON codec, Python→IR extraction
|
|
5
|
+
Project-URL: Homepage, https://github.com/eddiethedean/velarium
|
|
6
|
+
Project-URL: Repository, https://github.com/eddiethedean/velarium
|
|
7
|
+
Project-URL: Documentation, https://github.com/eddiethedean/velarium/blob/main/docs/README.md
|
|
8
|
+
Project-URL: Changelog, https://github.com/eddiethedean/velarium/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Velarium contributors
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: ir,modelspec,static-analysis,typing
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: typing-extensions>=4.2.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ty>=0.0.29; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# velarium
|
|
28
|
+
|
|
29
|
+
**Role in Velarium:** core **ModelSpec IR** — normalized types (`TypeSpec`, `TypeKind`, …), **`ModelSpec`**, JSON codec, union normalization, and builders that turn dataclasses and `TypedDict` into IR.
|
|
30
|
+
|
|
31
|
+
| | |
|
|
32
|
+
|---|---|
|
|
33
|
+
| **PyPI** | `velarium` |
|
|
34
|
+
| **Import** | `import velarium` / `from velarium.ir import ModelSpec, TypeSpec` |
|
|
35
|
+
| **Dependencies** | `typing_extensions` only |
|
|
36
|
+
|
|
37
|
+
Downstream packages (e.g. [**stubber**](../stubber/README.md)) consume this IR to emit `.pyi` stubs and other artifacts. The IR contract is specified in [docs/modelspec-ir.md](../../docs/modelspec-ir.md); ecosystem context is in [docs/valarium.md](../../docs/valarium.md) and [docs/design.md](../../docs/design.md).
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install velarium
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
From the monorepo root (with [uv](https://docs.astral.sh/uv/)):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv sync --group dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Version
|
|
52
|
+
|
|
53
|
+
`__version__` lives in `velarium/__init__.py` (Hatch reads it from that package’s `pyproject.toml`).
|
|
54
|
+
|
|
55
|
+
## See also
|
|
56
|
+
|
|
57
|
+
- [Repository README](../../README.md) — full package table and workspace setup
|
|
58
|
+
- [Documentation index](../../docs/README.md)
|
velarium-0.1.0/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# velarium
|
|
2
|
+
|
|
3
|
+
**Role in Velarium:** core **ModelSpec IR** — normalized types (`TypeSpec`, `TypeKind`, …), **`ModelSpec`**, JSON codec, union normalization, and builders that turn dataclasses and `TypedDict` into IR.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **PyPI** | `velarium` |
|
|
8
|
+
| **Import** | `import velarium` / `from velarium.ir import ModelSpec, TypeSpec` |
|
|
9
|
+
| **Dependencies** | `typing_extensions` only |
|
|
10
|
+
|
|
11
|
+
Downstream packages (e.g. [**stubber**](../stubber/README.md)) consume this IR to emit `.pyi` stubs and other artifacts. The IR contract is specified in [docs/modelspec-ir.md](../../docs/modelspec-ir.md); ecosystem context is in [docs/valarium.md](../../docs/valarium.md) and [docs/design.md](../../docs/design.md).
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install velarium
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
From the monorepo root (with [uv](https://docs.astral.sh/uv/)):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv sync --group dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Version
|
|
26
|
+
|
|
27
|
+
`__version__` lives in `velarium/__init__.py` (Hatch reads it from that package’s `pyproject.toml`).
|
|
28
|
+
|
|
29
|
+
## See also
|
|
30
|
+
|
|
31
|
+
- [Repository README](../../README.md) — full package table and workspace setup
|
|
32
|
+
- [Documentation index](../../docs/README.md)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "velarium"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Velarium IR (ModelSpec): normalization, JSON codec, Python→IR extraction"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Velarium contributors" }]
|
|
13
|
+
keywords = ["typing", "ir", "static-analysis", "modelspec"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"typing_extensions>=4.2.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=7.0", "ty>=0.0.29", "build>=1.0", "pytest-cov>=4.0"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/eddiethedean/velarium"
|
|
31
|
+
Repository = "https://github.com/eddiethedean/velarium"
|
|
32
|
+
Documentation = "https://github.com/eddiethedean/velarium/blob/main/docs/README.md"
|
|
33
|
+
Changelog = "https://github.com/eddiethedean/velarium/blob/main/CHANGELOG.md"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.version]
|
|
36
|
+
path = "velarium/__init__.py"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.sdist]
|
|
39
|
+
include = ["velarium", "README.md"]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist.force-include]
|
|
42
|
+
"../../LICENSE" = "LICENSE"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["velarium"]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Velarium: ModelSpec IR, normalization, JSON codec, and Python→IR extraction."""
|
|
2
|
+
|
|
3
|
+
from velarium.annotations import annotation_to_typespec, type_to_typespec
|
|
4
|
+
from velarium.ir import (
|
|
5
|
+
FieldSpec,
|
|
6
|
+
ModelConfig,
|
|
7
|
+
ModelMetadata,
|
|
8
|
+
ModelSpec,
|
|
9
|
+
TypeKind,
|
|
10
|
+
TypeSpec,
|
|
11
|
+
)
|
|
12
|
+
from velarium.json_codec import (
|
|
13
|
+
dumps_model_spec,
|
|
14
|
+
loads_model_spec,
|
|
15
|
+
model_spec_from_dict,
|
|
16
|
+
model_spec_to_dict,
|
|
17
|
+
)
|
|
18
|
+
from velarium.modelspec_build import (
|
|
19
|
+
modelspec_from_dataclass,
|
|
20
|
+
modelspec_from_typed_dict,
|
|
21
|
+
typespec_from_object,
|
|
22
|
+
)
|
|
23
|
+
from velarium.normalize import normalize_typespec, normalize_union, optional_to_union
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"annotation_to_typespec",
|
|
27
|
+
"type_to_typespec",
|
|
28
|
+
"FieldSpec",
|
|
29
|
+
"ModelConfig",
|
|
30
|
+
"ModelMetadata",
|
|
31
|
+
"ModelSpec",
|
|
32
|
+
"TypeKind",
|
|
33
|
+
"TypeSpec",
|
|
34
|
+
"dumps_model_spec",
|
|
35
|
+
"loads_model_spec",
|
|
36
|
+
"model_spec_from_dict",
|
|
37
|
+
"model_spec_to_dict",
|
|
38
|
+
"modelspec_from_dataclass",
|
|
39
|
+
"modelspec_from_typed_dict",
|
|
40
|
+
"typespec_from_object",
|
|
41
|
+
"normalize_typespec",
|
|
42
|
+
"normalize_union",
|
|
43
|
+
"optional_to_union",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Map Python typing objects to TypeSpec (MVP subset)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import typing
|
|
7
|
+
from collections.abc import Callable as CallableABC
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from types import UnionType
|
|
10
|
+
from typing import Any, ForwardRef, TypeVar, Union, get_args, get_origin
|
|
11
|
+
|
|
12
|
+
from velarium.ir import TypeKind, TypeSpec
|
|
13
|
+
from velarium.normalize import normalize_typespec, optional_to_union
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _none() -> TypeSpec:
|
|
17
|
+
return TypeSpec(kind=TypeKind.NONE)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _merge_optional(ts: TypeSpec, *, optional: bool) -> TypeSpec:
|
|
21
|
+
out = TypeSpec(
|
|
22
|
+
kind=ts.kind,
|
|
23
|
+
args=ts.args,
|
|
24
|
+
optional=optional or ts.optional,
|
|
25
|
+
nullable=ts.nullable,
|
|
26
|
+
default=ts.default,
|
|
27
|
+
)
|
|
28
|
+
return optional_to_union(out) if out.optional else out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def type_to_typespec(
|
|
32
|
+
t: Any,
|
|
33
|
+
*,
|
|
34
|
+
optional: bool = False,
|
|
35
|
+
) -> TypeSpec:
|
|
36
|
+
"""Convert a runtime typing object or builtin type to TypeSpec."""
|
|
37
|
+
if t is type(None) or t is None:
|
|
38
|
+
return _merge_optional(_none(), optional=optional)
|
|
39
|
+
|
|
40
|
+
if t is Any or t is typing.Any:
|
|
41
|
+
return _merge_optional(TypeSpec(kind=TypeKind.ANY), optional=optional)
|
|
42
|
+
|
|
43
|
+
if isinstance(t, ForwardRef):
|
|
44
|
+
return _merge_optional(TypeSpec(kind=TypeKind.ANY), optional=optional)
|
|
45
|
+
|
|
46
|
+
if isinstance(t, str):
|
|
47
|
+
return _merge_optional(TypeSpec(kind=TypeKind.ANY), optional=optional)
|
|
48
|
+
|
|
49
|
+
origin = get_origin(t)
|
|
50
|
+
args = get_args(t)
|
|
51
|
+
|
|
52
|
+
if origin is typing.Literal:
|
|
53
|
+
parts: list[TypeSpec] = []
|
|
54
|
+
for a in args:
|
|
55
|
+
parts.append(TypeSpec(kind=TypeKind.LITERAL, default=a))
|
|
56
|
+
if len(parts) == 1:
|
|
57
|
+
u = parts[0]
|
|
58
|
+
else:
|
|
59
|
+
u = TypeSpec(kind=TypeKind.UNION, args=parts)
|
|
60
|
+
return _merge_optional(normalize_typespec(u), optional=optional)
|
|
61
|
+
|
|
62
|
+
if origin is UnionType or origin is Union:
|
|
63
|
+
parts = [type_to_typespec(a, optional=False) for a in args]
|
|
64
|
+
has_none = any(p.kind == TypeKind.NONE for p in parts)
|
|
65
|
+
u = TypeSpec(kind=TypeKind.UNION, args=parts)
|
|
66
|
+
u = normalize_typespec(u)
|
|
67
|
+
opt = optional or has_none
|
|
68
|
+
if has_none:
|
|
69
|
+
u.optional = True
|
|
70
|
+
return _merge_optional(u, optional=opt)
|
|
71
|
+
|
|
72
|
+
if origin is list or origin is typing.List:
|
|
73
|
+
inner = (
|
|
74
|
+
type_to_typespec(args[0], optional=False)
|
|
75
|
+
if args
|
|
76
|
+
else TypeSpec(kind=TypeKind.ANY)
|
|
77
|
+
)
|
|
78
|
+
return _merge_optional(
|
|
79
|
+
TypeSpec(kind=TypeKind.LIST, args=[inner]), optional=optional
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if origin is dict or origin is typing.Dict:
|
|
83
|
+
k = (
|
|
84
|
+
type_to_typespec(args[0], optional=False)
|
|
85
|
+
if len(args) > 0
|
|
86
|
+
else TypeSpec(kind=TypeKind.ANY)
|
|
87
|
+
)
|
|
88
|
+
v = (
|
|
89
|
+
type_to_typespec(args[1], optional=False)
|
|
90
|
+
if len(args) > 1
|
|
91
|
+
else TypeSpec(kind=TypeKind.ANY)
|
|
92
|
+
)
|
|
93
|
+
return _merge_optional(
|
|
94
|
+
TypeSpec(kind=TypeKind.DICT, args=[k, v]), optional=optional
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if origin is set or origin is typing.Set:
|
|
98
|
+
inner = (
|
|
99
|
+
type_to_typespec(args[0], optional=False)
|
|
100
|
+
if args
|
|
101
|
+
else TypeSpec(kind=TypeKind.ANY)
|
|
102
|
+
)
|
|
103
|
+
return _merge_optional(
|
|
104
|
+
TypeSpec(kind=TypeKind.SET, args=[inner]), optional=optional
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if origin is tuple or origin is typing.Tuple:
|
|
108
|
+
if not args:
|
|
109
|
+
inner = TypeSpec(kind=TypeKind.ANY)
|
|
110
|
+
return _merge_optional(
|
|
111
|
+
TypeSpec(kind=TypeKind.TUPLE, args=[inner]), optional=optional
|
|
112
|
+
)
|
|
113
|
+
if len(args) == 2 and args[1] is Ellipsis:
|
|
114
|
+
inner = type_to_typespec(args[0], optional=False)
|
|
115
|
+
return _merge_optional(
|
|
116
|
+
TypeSpec(kind=TypeKind.TUPLE, args=[inner]), optional=optional
|
|
117
|
+
)
|
|
118
|
+
inners = [type_to_typespec(a, optional=False) for a in args]
|
|
119
|
+
return _merge_optional(
|
|
120
|
+
TypeSpec(kind=TypeKind.TUPLE, args=inners), optional=optional
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if origin is typing.Callable or origin is CallableABC:
|
|
124
|
+
if args:
|
|
125
|
+
param_args, ret = args[0], args[1]
|
|
126
|
+
if (
|
|
127
|
+
get_origin(param_args) is list
|
|
128
|
+
): # pragma: no cover — rare Callable[..., T] shape; else path covers real callables
|
|
129
|
+
plist = get_args(param_args)
|
|
130
|
+
plist_ts = TypeSpec(
|
|
131
|
+
kind=TypeKind.LIST,
|
|
132
|
+
args=[type_to_typespec(p, optional=False) for p in plist],
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
plist_ts = type_to_typespec(param_args, optional=False)
|
|
136
|
+
ret_ts = type_to_typespec(ret, optional=False)
|
|
137
|
+
return _merge_optional(
|
|
138
|
+
TypeSpec(kind=TypeKind.CALLABLE, args=[plist_ts, ret_ts]),
|
|
139
|
+
optional=optional,
|
|
140
|
+
)
|
|
141
|
+
return _merge_optional(
|
|
142
|
+
TypeSpec(kind=TypeKind.CALLABLE, args=None), optional=optional
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if isinstance(t, TypeVar):
|
|
146
|
+
return _merge_optional(TypeSpec(kind=TypeKind.TYPE_VAR), optional=optional)
|
|
147
|
+
|
|
148
|
+
if origin is not None:
|
|
149
|
+
if args:
|
|
150
|
+
inner_args = [type_to_typespec(a, optional=False) for a in args]
|
|
151
|
+
return _merge_optional(
|
|
152
|
+
TypeSpec(kind=TypeKind.GENERIC, args=inner_args), optional=optional
|
|
153
|
+
)
|
|
154
|
+
return _merge_optional(
|
|
155
|
+
TypeSpec(kind=TypeKind.GENERIC, args=[]), optional=optional
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if isinstance(t, type):
|
|
159
|
+
if t is int:
|
|
160
|
+
return _merge_optional(TypeSpec(kind=TypeKind.INT), optional=optional)
|
|
161
|
+
if t is float:
|
|
162
|
+
return _merge_optional(TypeSpec(kind=TypeKind.FLOAT), optional=optional)
|
|
163
|
+
if t is bool:
|
|
164
|
+
return _merge_optional(TypeSpec(kind=TypeKind.BOOL), optional=optional)
|
|
165
|
+
if t is str:
|
|
166
|
+
return _merge_optional(TypeSpec(kind=TypeKind.STR), optional=optional)
|
|
167
|
+
if t is bytes:
|
|
168
|
+
return _merge_optional(TypeSpec(kind=TypeKind.BYTES), optional=optional)
|
|
169
|
+
if t is datetime.datetime:
|
|
170
|
+
return _merge_optional(TypeSpec(kind=TypeKind.DATETIME), optional=optional)
|
|
171
|
+
if t is datetime.date:
|
|
172
|
+
return _merge_optional(TypeSpec(kind=TypeKind.DATE), optional=optional)
|
|
173
|
+
if t is datetime.time:
|
|
174
|
+
return _merge_optional(TypeSpec(kind=TypeKind.TIME), optional=optional)
|
|
175
|
+
if issubclass(t, Enum):
|
|
176
|
+
members = [TypeSpec(kind=TypeKind.LITERAL, default=m.value) for m in t]
|
|
177
|
+
u = (
|
|
178
|
+
TypeSpec(kind=TypeKind.ENUM, args=members)
|
|
179
|
+
if members
|
|
180
|
+
else TypeSpec(kind=TypeKind.ANY)
|
|
181
|
+
)
|
|
182
|
+
return _merge_optional(u, optional=optional)
|
|
183
|
+
return _merge_optional(TypeSpec(kind=TypeKind.ANY), optional=optional)
|
|
184
|
+
|
|
185
|
+
return _merge_optional(TypeSpec(kind=TypeKind.ANY), optional=optional)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def annotation_to_typespec(
|
|
189
|
+
annotation: Any, *, globalns: dict[str, Any] | None = None
|
|
190
|
+
) -> TypeSpec:
|
|
191
|
+
"""Resolve string annotations and return TypeSpec."""
|
|
192
|
+
if isinstance(annotation, str):
|
|
193
|
+
if globalns is None:
|
|
194
|
+
return TypeSpec(kind=TypeKind.ANY)
|
|
195
|
+
try:
|
|
196
|
+
anno = eval(annotation, globalns, globalns) # noqa: S307
|
|
197
|
+
return normalize_typespec(type_to_typespec(anno, optional=False))
|
|
198
|
+
except Exception:
|
|
199
|
+
return TypeSpec(kind=TypeKind.ANY)
|
|
200
|
+
return normalize_typespec(type_to_typespec(annotation, optional=False))
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""ModelSpec intermediate representation (see docs/modelspec-ir.md)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TypeKind(str, Enum):
|
|
11
|
+
# Primitives
|
|
12
|
+
INT = "int"
|
|
13
|
+
FLOAT = "float"
|
|
14
|
+
BOOL = "bool"
|
|
15
|
+
STR = "str"
|
|
16
|
+
BYTES = "bytes"
|
|
17
|
+
|
|
18
|
+
# Complex
|
|
19
|
+
LIST = "list"
|
|
20
|
+
TUPLE = "tuple"
|
|
21
|
+
DICT = "dict"
|
|
22
|
+
SET = "set"
|
|
23
|
+
|
|
24
|
+
# Special
|
|
25
|
+
UNION = "union"
|
|
26
|
+
LITERAL = "literal"
|
|
27
|
+
ENUM = "enum"
|
|
28
|
+
ANY = "any"
|
|
29
|
+
NONE = "none"
|
|
30
|
+
|
|
31
|
+
# Temporal
|
|
32
|
+
DATETIME = "datetime"
|
|
33
|
+
DATE = "date"
|
|
34
|
+
TIME = "time"
|
|
35
|
+
TIMESTAMP = "timestamp"
|
|
36
|
+
|
|
37
|
+
# Advanced
|
|
38
|
+
GENERIC = "generic"
|
|
39
|
+
CALLABLE = "callable"
|
|
40
|
+
TYPE_VAR = "typevar"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class TypeSpec:
|
|
45
|
+
kind: TypeKind
|
|
46
|
+
args: list[TypeSpec] | None = None
|
|
47
|
+
optional: bool = False
|
|
48
|
+
nullable: bool = False
|
|
49
|
+
default: Any | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ModelConfig:
|
|
54
|
+
frozen: bool = False
|
|
55
|
+
extra: Literal["allow", "forbid", "ignore"] = "forbid"
|
|
56
|
+
validate_assignment: bool = True
|
|
57
|
+
from_attributes: bool = True
|
|
58
|
+
strict: bool = False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class FieldSpec:
|
|
63
|
+
name: str
|
|
64
|
+
type: TypeSpec
|
|
65
|
+
required: bool
|
|
66
|
+
default: Any | None = None
|
|
67
|
+
alias: str | None = None
|
|
68
|
+
description: str | None = None
|
|
69
|
+
deprecated: bool = False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ModelMetadata:
|
|
74
|
+
source_module: str | None = None
|
|
75
|
+
source_file: str | None = None
|
|
76
|
+
line_number: int | None = None
|
|
77
|
+
generated_by: str = "velarium"
|
|
78
|
+
version: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class ModelSpec:
|
|
83
|
+
name: str
|
|
84
|
+
fields: dict[str, TypeSpec] = field(default_factory=dict)
|
|
85
|
+
config: ModelConfig | None = None
|
|
86
|
+
metadata: ModelMetadata | None = None
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""JSON serialization for ModelSpec IR (deterministic, round-trippable)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from velarium.ir import (
|
|
10
|
+
FieldSpec,
|
|
11
|
+
ModelConfig,
|
|
12
|
+
ModelMetadata,
|
|
13
|
+
ModelSpec,
|
|
14
|
+
TypeKind,
|
|
15
|
+
TypeSpec,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _typespec_from_dict(d: dict[str, Any]) -> TypeSpec:
|
|
20
|
+
args = d.get("args")
|
|
21
|
+
if args is not None:
|
|
22
|
+
args = [_typespec_from_dict(x) if isinstance(x, dict) else x for x in args]
|
|
23
|
+
return TypeSpec(
|
|
24
|
+
kind=TypeKind(d["kind"]),
|
|
25
|
+
args=args,
|
|
26
|
+
optional=d.get("optional", False),
|
|
27
|
+
nullable=d.get("nullable", False),
|
|
28
|
+
default=d.get("default", None),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _json_default_value(value: Any) -> Any:
|
|
33
|
+
if isinstance(value, Enum):
|
|
34
|
+
return value.value
|
|
35
|
+
try:
|
|
36
|
+
json.dumps(value)
|
|
37
|
+
except TypeError:
|
|
38
|
+
return {"_velarium_repr": repr(value)}
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def typespec_to_dict(ts: TypeSpec) -> dict[str, Any]:
|
|
43
|
+
out: dict[str, Any] = {"kind": ts.kind.value}
|
|
44
|
+
if ts.args is not None:
|
|
45
|
+
out["args"] = [typespec_to_dict(a) for a in ts.args]
|
|
46
|
+
if ts.optional:
|
|
47
|
+
out["optional"] = True
|
|
48
|
+
if ts.nullable:
|
|
49
|
+
out["nullable"] = True
|
|
50
|
+
if ts.default is not None:
|
|
51
|
+
out["default"] = _json_default_value(ts.default)
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def typespec_from_dict(d: dict[str, Any]) -> TypeSpec:
|
|
56
|
+
return _typespec_from_dict(d)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def model_spec_to_dict(m: ModelSpec) -> dict[str, Any]:
|
|
60
|
+
out: dict[str, Any] = {
|
|
61
|
+
"name": m.name,
|
|
62
|
+
"fields": {k: typespec_to_dict(v) for k, v in m.fields.items()},
|
|
63
|
+
}
|
|
64
|
+
if m.config is not None:
|
|
65
|
+
c = m.config
|
|
66
|
+
out["config"] = {
|
|
67
|
+
"frozen": c.frozen,
|
|
68
|
+
"extra": c.extra,
|
|
69
|
+
"validate_assignment": c.validate_assignment,
|
|
70
|
+
"from_attributes": c.from_attributes,
|
|
71
|
+
"strict": c.strict,
|
|
72
|
+
}
|
|
73
|
+
if m.metadata is not None:
|
|
74
|
+
md = m.metadata
|
|
75
|
+
out["metadata"] = {
|
|
76
|
+
"source_module": md.source_module,
|
|
77
|
+
"source_file": md.source_file,
|
|
78
|
+
"line_number": md.line_number,
|
|
79
|
+
"generated_by": md.generated_by,
|
|
80
|
+
"version": md.version,
|
|
81
|
+
}
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def model_spec_from_dict(d: dict[str, Any]) -> ModelSpec:
|
|
86
|
+
fields_raw = d.get("fields") or {}
|
|
87
|
+
fields = {k: typespec_from_dict(v) for k, v in fields_raw.items()}
|
|
88
|
+
config = None
|
|
89
|
+
if "config" in d and d["config"] is not None:
|
|
90
|
+
c = d["config"]
|
|
91
|
+
config = ModelConfig(
|
|
92
|
+
frozen=c.get("frozen", False),
|
|
93
|
+
extra=c.get("extra", "forbid"),
|
|
94
|
+
validate_assignment=c.get("validate_assignment", True),
|
|
95
|
+
from_attributes=c.get("from_attributes", True),
|
|
96
|
+
strict=c.get("strict", False),
|
|
97
|
+
)
|
|
98
|
+
metadata = None
|
|
99
|
+
if "metadata" in d and d["metadata"] is not None:
|
|
100
|
+
md = d["metadata"]
|
|
101
|
+
metadata = ModelMetadata(
|
|
102
|
+
source_module=md.get("source_module"),
|
|
103
|
+
source_file=md.get("source_file"),
|
|
104
|
+
line_number=md.get("line_number"),
|
|
105
|
+
generated_by=md.get("generated_by", "velarium"),
|
|
106
|
+
version=md.get("version"),
|
|
107
|
+
)
|
|
108
|
+
return ModelSpec(
|
|
109
|
+
name=d["name"],
|
|
110
|
+
fields=fields,
|
|
111
|
+
config=config,
|
|
112
|
+
metadata=metadata,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def field_spec_to_dict(f: FieldSpec) -> dict[str, Any]:
|
|
117
|
+
out: dict[str, Any] = {
|
|
118
|
+
"name": f.name,
|
|
119
|
+
"type": typespec_to_dict(f.type),
|
|
120
|
+
"required": f.required,
|
|
121
|
+
"default": f.default,
|
|
122
|
+
"alias": f.alias,
|
|
123
|
+
"description": f.description,
|
|
124
|
+
"deprecated": f.deprecated,
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
k: v
|
|
128
|
+
for k, v in out.items()
|
|
129
|
+
if v is not None or k in ("name", "type", "required")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def field_spec_from_dict(d: dict[str, Any]) -> FieldSpec:
|
|
134
|
+
return FieldSpec(
|
|
135
|
+
name=d["name"],
|
|
136
|
+
type=typespec_from_dict(d["type"]),
|
|
137
|
+
required=d["required"],
|
|
138
|
+
default=d.get("default"),
|
|
139
|
+
alias=d.get("alias"),
|
|
140
|
+
description=d.get("description"),
|
|
141
|
+
deprecated=d.get("deprecated", False),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def dumps_model_spec(m: ModelSpec, *, indent: int | None = 2) -> str:
|
|
146
|
+
"""Serialize ModelSpec to JSON with stable key ordering."""
|
|
147
|
+
return json.dumps(
|
|
148
|
+
model_spec_to_dict(m),
|
|
149
|
+
indent=indent,
|
|
150
|
+
sort_keys=True,
|
|
151
|
+
default=_json_default_value,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def loads_model_spec(s: str) -> ModelSpec:
|
|
156
|
+
return model_spec_from_dict(json.loads(s))
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Build ModelSpec from Python classes (dataclasses MVP)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import inspect
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, get_type_hints
|
|
9
|
+
|
|
10
|
+
from typing_extensions import is_typeddict
|
|
11
|
+
|
|
12
|
+
from velarium.annotations import annotation_to_typespec, type_to_typespec
|
|
13
|
+
from velarium.ir import ModelConfig, ModelMetadata, ModelSpec, TypeSpec
|
|
14
|
+
from velarium.normalize import normalize_typespec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _module_globals(cls: type) -> dict[str, Any]:
|
|
18
|
+
mod = sys.modules.get(cls.__module__)
|
|
19
|
+
if mod is None:
|
|
20
|
+
return dict(vars(cls))
|
|
21
|
+
g = dict(vars(mod))
|
|
22
|
+
g.setdefault(cls.__name__, cls)
|
|
23
|
+
return g
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def modelspec_from_dataclass(cls: type, *, include_extras: bool = False) -> ModelSpec:
|
|
27
|
+
"""Extract ModelSpec from a dataclass type."""
|
|
28
|
+
if not dataclasses.is_dataclass(cls):
|
|
29
|
+
raise TypeError(f"{cls!r} is not a dataclass")
|
|
30
|
+
|
|
31
|
+
globalns = _module_globals(cls)
|
|
32
|
+
try:
|
|
33
|
+
hints = get_type_hints(
|
|
34
|
+
cls, globalns=globalns, localns=None, include_extras=include_extras
|
|
35
|
+
)
|
|
36
|
+
except Exception:
|
|
37
|
+
hints = {}
|
|
38
|
+
|
|
39
|
+
fields: dict[str, TypeSpec] = {}
|
|
40
|
+
for f in dataclasses.fields(cls):
|
|
41
|
+
if not f.init:
|
|
42
|
+
continue
|
|
43
|
+
raw = hints.get(f.name, f.type if hasattr(f, "type") else Any)
|
|
44
|
+
ts = annotation_to_typespec(raw, globalns=globalns)
|
|
45
|
+
ts = normalize_typespec(ts)
|
|
46
|
+
if f.default is not dataclasses.MISSING:
|
|
47
|
+
ts = TypeSpec(
|
|
48
|
+
kind=ts.kind,
|
|
49
|
+
args=ts.args,
|
|
50
|
+
optional=ts.optional,
|
|
51
|
+
nullable=ts.nullable,
|
|
52
|
+
default=f.default,
|
|
53
|
+
)
|
|
54
|
+
fields[f.name] = ts
|
|
55
|
+
|
|
56
|
+
src_file = None
|
|
57
|
+
line_number = None
|
|
58
|
+
try:
|
|
59
|
+
src_file = inspect.getsourcefile(cls)
|
|
60
|
+
lines, start = inspect.getsourcelines(cls)
|
|
61
|
+
line_number = start
|
|
62
|
+
except (OSError, TypeError):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
meta = ModelMetadata(
|
|
66
|
+
source_module=cls.__module__,
|
|
67
|
+
source_file=src_file,
|
|
68
|
+
line_number=line_number,
|
|
69
|
+
generated_by="velarium",
|
|
70
|
+
version=None,
|
|
71
|
+
)
|
|
72
|
+
params = getattr(cls, "__dataclass_params__", None)
|
|
73
|
+
frozen = bool(getattr(params, "frozen", False)) if params is not None else False
|
|
74
|
+
cfg = ModelConfig(frozen=frozen, extra="forbid")
|
|
75
|
+
|
|
76
|
+
return ModelSpec(name=cls.__name__, fields=fields, config=cfg, metadata=meta)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def modelspec_from_typed_dict(cls: type) -> ModelSpec:
|
|
80
|
+
"""Extract ModelSpec from typing.TypedDict (total fields only for MVP)."""
|
|
81
|
+
if not is_typeddict(cls):
|
|
82
|
+
raise TypeError(f"{cls!r} is not a TypedDict")
|
|
83
|
+
|
|
84
|
+
globalns = _module_globals(cls)
|
|
85
|
+
try:
|
|
86
|
+
hints = get_type_hints(cls, globalns=globalns)
|
|
87
|
+
except Exception:
|
|
88
|
+
hints = {}
|
|
89
|
+
|
|
90
|
+
fields: dict[str, TypeSpec] = {}
|
|
91
|
+
for name, typ in getattr(cls, "__annotations__", {}).items():
|
|
92
|
+
ts = annotation_to_typespec(hints.get(name, typ), globalns=globalns)
|
|
93
|
+
fields[name] = normalize_typespec(ts)
|
|
94
|
+
|
|
95
|
+
meta = ModelMetadata(source_module=cls.__module__, generated_by="velarium")
|
|
96
|
+
return ModelSpec(name=cls.__name__, fields=fields, config=None, metadata=meta)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def typespec_from_object(obj: Any) -> TypeSpec:
|
|
100
|
+
"""Convenience: typing object or type -> TypeSpec."""
|
|
101
|
+
return normalize_typespec(type_to_typespec(obj, optional=False))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Normalize TypeSpec unions per docs/modelspec-ir.md (flatten, dedupe, preserve order)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from velarium.ir import TypeKind, TypeSpec
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize_union(ts: TypeSpec) -> TypeSpec:
|
|
9
|
+
"""Flatten nested unions, remove duplicates, preserve first-seen order."""
|
|
10
|
+
if ts.kind != TypeKind.UNION:
|
|
11
|
+
return ts
|
|
12
|
+
flat: list[TypeSpec] = []
|
|
13
|
+
seen: list[str] = []
|
|
14
|
+
|
|
15
|
+
def json_key_obj(d: object) -> str:
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
return json.dumps(d, sort_keys=True)
|
|
19
|
+
|
|
20
|
+
def key(x: TypeSpec) -> str:
|
|
21
|
+
from velarium.json_codec import typespec_to_dict
|
|
22
|
+
|
|
23
|
+
return json_key_obj(typespec_to_dict(x))
|
|
24
|
+
|
|
25
|
+
stack = list(ts.args or [])
|
|
26
|
+
while stack:
|
|
27
|
+
cur = stack.pop(0)
|
|
28
|
+
if cur.kind == TypeKind.UNION and cur.args:
|
|
29
|
+
stack = list(cur.args) + stack
|
|
30
|
+
continue
|
|
31
|
+
k = key(cur)
|
|
32
|
+
if k not in seen:
|
|
33
|
+
seen.append(k)
|
|
34
|
+
flat.append(cur)
|
|
35
|
+
if len(flat) == 1:
|
|
36
|
+
u = flat[0]
|
|
37
|
+
return TypeSpec(
|
|
38
|
+
kind=u.kind,
|
|
39
|
+
args=u.args,
|
|
40
|
+
optional=ts.optional or u.optional,
|
|
41
|
+
nullable=ts.nullable or u.nullable,
|
|
42
|
+
default=ts.default if ts.default is not None else u.default,
|
|
43
|
+
)
|
|
44
|
+
return TypeSpec(
|
|
45
|
+
kind=TypeKind.UNION,
|
|
46
|
+
args=flat,
|
|
47
|
+
optional=ts.optional,
|
|
48
|
+
nullable=ts.nullable,
|
|
49
|
+
default=ts.default,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_typespec(ts: TypeSpec) -> TypeSpec:
|
|
54
|
+
"""Recursively normalize unions and nested structures."""
|
|
55
|
+
args = ts.args
|
|
56
|
+
if args:
|
|
57
|
+
args = [normalize_typespec(a) for a in args]
|
|
58
|
+
out = TypeSpec(
|
|
59
|
+
kind=ts.kind,
|
|
60
|
+
args=args,
|
|
61
|
+
optional=ts.optional,
|
|
62
|
+
nullable=ts.nullable,
|
|
63
|
+
default=ts.default,
|
|
64
|
+
)
|
|
65
|
+
if out.kind == TypeKind.UNION:
|
|
66
|
+
return normalize_union(out)
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def optional_to_union(ts: TypeSpec) -> TypeSpec:
|
|
71
|
+
"""
|
|
72
|
+
Encode Optional[T] as Union[T, None] with optional=True per spec.
|
|
73
|
+
If ts already includes none, ensure optional flag is set.
|
|
74
|
+
"""
|
|
75
|
+
if not ts.optional:
|
|
76
|
+
return ts
|
|
77
|
+
none = TypeSpec(kind=TypeKind.NONE)
|
|
78
|
+
if ts.kind == TypeKind.UNION and ts.args:
|
|
79
|
+
members = list(ts.args)
|
|
80
|
+
has_none = any(m.kind == TypeKind.NONE for m in members)
|
|
81
|
+
if not has_none:
|
|
82
|
+
members.append(none)
|
|
83
|
+
u = TypeSpec(
|
|
84
|
+
kind=TypeKind.UNION,
|
|
85
|
+
args=members,
|
|
86
|
+
optional=True,
|
|
87
|
+
nullable=ts.nullable,
|
|
88
|
+
default=ts.default,
|
|
89
|
+
)
|
|
90
|
+
return normalize_union(u)
|
|
91
|
+
u = TypeSpec(
|
|
92
|
+
kind=TypeKind.UNION,
|
|
93
|
+
args=[ts, none],
|
|
94
|
+
optional=True,
|
|
95
|
+
nullable=ts.nullable,
|
|
96
|
+
default=ts.default,
|
|
97
|
+
)
|
|
98
|
+
return normalize_union(u)
|
|
File without changes
|