trolskgen 0.0.1__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.
- trolskgen-0.0.1/LICENCE +21 -0
- trolskgen-0.0.1/PKG-INFO +41 -0
- trolskgen-0.0.1/README.md +21 -0
- trolskgen-0.0.1/pyproject.toml +37 -0
- trolskgen-0.0.1/setup.cfg +4 -0
- trolskgen-0.0.1/src/trolskgen/__init__.py +6 -0
- trolskgen-0.0.1/src/trolskgen/ast_types.py +205 -0
- trolskgen-0.0.1/src/trolskgen/converters.py +294 -0
- trolskgen-0.0.1/src/trolskgen/core.py +84 -0
- trolskgen-0.0.1/src/trolskgen/templates.py +84 -0
- trolskgen-0.0.1/src/trolskgen.egg-info/PKG-INFO +41 -0
- trolskgen-0.0.1/src/trolskgen.egg-info/SOURCES.txt +15 -0
- trolskgen-0.0.1/src/trolskgen.egg-info/dependency_links.txt +1 -0
- trolskgen-0.0.1/src/trolskgen.egg-info/requires.txt +7 -0
- trolskgen-0.0.1/src/trolskgen.egg-info/top_level.txt +1 -0
- trolskgen-0.0.1/tests/test_template.py +27 -0
- trolskgen-0.0.1/tests/test_trolskgen.py +548 -0
trolskgen-0.0.1/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Oliver Russell
|
|
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.
|
trolskgen-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trolskgen
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Cute Python codegen
|
|
5
|
+
Author-email: Oliver Russell <ojhrussell@gmail.com>
|
|
6
|
+
Maintainer-email: Oliver Russell <ojhrussell@gmail.com>
|
|
7
|
+
Project-URL: homepage, https://github.com/leontrolski/trolskgen
|
|
8
|
+
Keywords: codegen
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENCE
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest==8.3.5; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff==0.11.2; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy==1.15.0; extra == "dev"
|
|
17
|
+
Requires-Dist: rich==14.0.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pydantic==2.11.7; extra == "dev"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Trolskgen
|
|
22
|
+
|
|
23
|
+
# Hello World
|
|
24
|
+
|
|
25
|
+
After `pip install trolskgen`, just run:
|
|
26
|
+
|
|
27
|
+
# TODO
|
|
28
|
+
|
|
29
|
+
- README
|
|
30
|
+
- Walk through each converter
|
|
31
|
+
- Plugins example
|
|
32
|
+
- Implement comments somehow? Monkeypatch ast?
|
|
33
|
+
- Notes on comments - use docstrings etc.
|
|
34
|
+
|
|
35
|
+
# Development
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
uv pip install -e '.[dev]'
|
|
39
|
+
mypy .
|
|
40
|
+
pytest -vv
|
|
41
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Trolskgen
|
|
2
|
+
|
|
3
|
+
# Hello World
|
|
4
|
+
|
|
5
|
+
After `pip install trolskgen`, just run:
|
|
6
|
+
|
|
7
|
+
# TODO
|
|
8
|
+
|
|
9
|
+
- README
|
|
10
|
+
- Walk through each converter
|
|
11
|
+
- Plugins example
|
|
12
|
+
- Implement comments somehow? Monkeypatch ast?
|
|
13
|
+
- Notes on comments - use docstrings etc.
|
|
14
|
+
|
|
15
|
+
# Development
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
uv pip install -e '.[dev]'
|
|
19
|
+
mypy .
|
|
20
|
+
pytest -vv
|
|
21
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "trolskgen"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Cute Python codegen"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = {file = "LICENSE"}
|
|
8
|
+
keywords = ["codegen"]
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Oliver Russell", email = "ojhrussell@gmail.com"},
|
|
11
|
+
]
|
|
12
|
+
maintainers = [
|
|
13
|
+
{name = "Oliver Russell", email = "ojhrussell@gmail.com"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
dependencies = []
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest==8.3.5",
|
|
24
|
+
"ruff==0.11.2",
|
|
25
|
+
"mypy==1.15.0",
|
|
26
|
+
"rich==14.0.0",
|
|
27
|
+
"pydantic==2.11.7",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
homepage = "https://github.com/leontrolski/trolskgen"
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 120
|
|
35
|
+
|
|
36
|
+
[tool.mypy]
|
|
37
|
+
strict = true
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from trolskgen.core import Config as Config
|
|
2
|
+
from trolskgen.core import F as F
|
|
3
|
+
from trolskgen.core import TrolskgenError as TrolskgenError
|
|
4
|
+
from trolskgen.core import to_ast as to_ast
|
|
5
|
+
from trolskgen.core import to_source as to_source
|
|
6
|
+
from trolskgen.templates import t as t
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import sys
|
|
3
|
+
from types import NoneType
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
FIELD_MAPS: dict[type[Any], dict[str, Any]] = {
|
|
7
|
+
NoneType: {},
|
|
8
|
+
str: {},
|
|
9
|
+
int: {},
|
|
10
|
+
object: {},
|
|
11
|
+
ast.AST: {},
|
|
12
|
+
ast.mod: {},
|
|
13
|
+
ast.Module: {"body": list[ast.stmt], "type_ignores": list[ast.type_ignore]},
|
|
14
|
+
ast.Interactive: {"body": list[ast.stmt]},
|
|
15
|
+
ast.Expression: {"body": ast.expr},
|
|
16
|
+
ast.FunctionType: {"argtypes": list[ast.expr], "returns": ast.expr},
|
|
17
|
+
ast.stmt: {},
|
|
18
|
+
ast.FunctionDef: {
|
|
19
|
+
"name": str,
|
|
20
|
+
"args": ast.arguments,
|
|
21
|
+
"body": list[ast.stmt],
|
|
22
|
+
"decorator_list": list[ast.expr],
|
|
23
|
+
"returns": ast.expr | NoneType,
|
|
24
|
+
"type_comment": str | NoneType,
|
|
25
|
+
"type_params": list[ast.type_param],
|
|
26
|
+
},
|
|
27
|
+
ast.AsyncFunctionDef: {
|
|
28
|
+
"name": str,
|
|
29
|
+
"args": ast.arguments,
|
|
30
|
+
"body": list[ast.stmt],
|
|
31
|
+
"decorator_list": list[ast.expr],
|
|
32
|
+
"returns": ast.expr | NoneType,
|
|
33
|
+
"type_comment": str | NoneType,
|
|
34
|
+
"type_params": list[ast.type_param],
|
|
35
|
+
},
|
|
36
|
+
ast.ClassDef: {
|
|
37
|
+
"name": str,
|
|
38
|
+
"bases": list[ast.expr],
|
|
39
|
+
"keywords": list[ast.keyword],
|
|
40
|
+
"body": list[ast.stmt],
|
|
41
|
+
"decorator_list": list[ast.expr],
|
|
42
|
+
"type_params": list[ast.type_param],
|
|
43
|
+
},
|
|
44
|
+
ast.Return: {"value": ast.expr | NoneType},
|
|
45
|
+
ast.Delete: {"targets": list[ast.expr]},
|
|
46
|
+
ast.Assign: {"targets": list[ast.expr], "value": ast.expr, "type_comment": str | NoneType},
|
|
47
|
+
ast.TypeAlias: {"name": ast.expr, "type_params": list[ast.type_param], "value": ast.expr},
|
|
48
|
+
ast.AugAssign: {"target": ast.expr, "op": ast.operator, "value": ast.expr},
|
|
49
|
+
ast.AnnAssign: {"target": ast.expr, "annotation": ast.expr, "value": ast.expr | NoneType, "simple": int},
|
|
50
|
+
ast.For: {
|
|
51
|
+
"target": ast.expr,
|
|
52
|
+
"iter": ast.expr,
|
|
53
|
+
"body": list[ast.stmt],
|
|
54
|
+
"orelse": list[ast.stmt],
|
|
55
|
+
"type_comment": str | NoneType,
|
|
56
|
+
},
|
|
57
|
+
ast.AsyncFor: {
|
|
58
|
+
"target": ast.expr,
|
|
59
|
+
"iter": ast.expr,
|
|
60
|
+
"body": list[ast.stmt],
|
|
61
|
+
"orelse": list[ast.stmt],
|
|
62
|
+
"type_comment": str | NoneType,
|
|
63
|
+
},
|
|
64
|
+
ast.While: {"test": ast.expr, "body": list[ast.stmt], "orelse": list[ast.stmt]},
|
|
65
|
+
ast.If: {"test": ast.expr, "body": list[ast.stmt], "orelse": list[ast.stmt]},
|
|
66
|
+
ast.With: {"items": list[ast.withitem], "body": list[ast.stmt], "type_comment": str | NoneType},
|
|
67
|
+
ast.AsyncWith: {"items": list[ast.withitem], "body": list[ast.stmt], "type_comment": str | NoneType},
|
|
68
|
+
ast.Match: {"subject": ast.expr, "cases": list[ast.match_case]},
|
|
69
|
+
ast.Raise: {"exc": ast.expr | NoneType, "cause": ast.expr | NoneType},
|
|
70
|
+
ast.Try: {
|
|
71
|
+
"body": list[ast.stmt],
|
|
72
|
+
"handlers": list[ast.excepthandler],
|
|
73
|
+
"orelse": list[ast.stmt],
|
|
74
|
+
"finalbody": list[ast.stmt],
|
|
75
|
+
},
|
|
76
|
+
ast.TryStar: {
|
|
77
|
+
"body": list[ast.stmt],
|
|
78
|
+
"handlers": list[ast.excepthandler],
|
|
79
|
+
"orelse": list[ast.stmt],
|
|
80
|
+
"finalbody": list[ast.stmt],
|
|
81
|
+
},
|
|
82
|
+
ast.Assert: {"test": ast.expr, "msg": ast.expr | NoneType},
|
|
83
|
+
ast.Import: {"names": list[ast.alias]},
|
|
84
|
+
ast.ImportFrom: {"module": str | NoneType, "names": list[ast.alias], "level": int | NoneType},
|
|
85
|
+
ast.Global: {"names": list[str]},
|
|
86
|
+
ast.Nonlocal: {"names": list[str]},
|
|
87
|
+
ast.Expr: {"value": ast.expr},
|
|
88
|
+
ast.Pass: {},
|
|
89
|
+
ast.Break: {},
|
|
90
|
+
ast.Continue: {},
|
|
91
|
+
ast.expr: {},
|
|
92
|
+
ast.BoolOp: {"op": ast.boolop, "values": list[ast.expr]},
|
|
93
|
+
ast.NamedExpr: {"target": ast.expr, "value": ast.expr},
|
|
94
|
+
ast.BinOp: {"left": ast.expr, "op": ast.operator, "right": ast.expr},
|
|
95
|
+
ast.UnaryOp: {"op": ast.unaryop, "operand": ast.expr},
|
|
96
|
+
ast.Lambda: {"args": ast.arguments, "body": ast.expr},
|
|
97
|
+
ast.IfExp: {"test": ast.expr, "body": ast.expr, "orelse": ast.expr},
|
|
98
|
+
ast.Dict: {"keys": list[ast.expr], "values": list[ast.expr]},
|
|
99
|
+
ast.Set: {"elts": list[ast.expr]},
|
|
100
|
+
ast.ListComp: {"elt": ast.expr, "generators": list[ast.comprehension]},
|
|
101
|
+
ast.SetComp: {"elt": ast.expr, "generators": list[ast.comprehension]},
|
|
102
|
+
ast.DictComp: {"key": ast.expr, "value": ast.expr, "generators": list[ast.comprehension]},
|
|
103
|
+
ast.GeneratorExp: {"elt": ast.expr, "generators": list[ast.comprehension]},
|
|
104
|
+
ast.Await: {"value": ast.expr},
|
|
105
|
+
ast.Yield: {"value": ast.expr | NoneType},
|
|
106
|
+
ast.YieldFrom: {"value": ast.expr},
|
|
107
|
+
ast.Compare: {"left": ast.expr, "ops": list[ast.cmpop], "comparators": list[ast.expr]},
|
|
108
|
+
ast.Call: {"func": ast.expr, "args": list[ast.expr], "keywords": list[ast.keyword]},
|
|
109
|
+
ast.FormattedValue: {"value": ast.expr, "conversion": int, "format_spec": ast.expr | NoneType},
|
|
110
|
+
ast.JoinedStr: {"values": list[ast.expr]},
|
|
111
|
+
ast.Constant: {"value": object, "kind": str | NoneType},
|
|
112
|
+
ast.Attribute: {"value": ast.expr, "attr": str},
|
|
113
|
+
ast.Subscript: {"value": ast.expr, "slice": ast.expr},
|
|
114
|
+
ast.Starred: {"value": ast.expr},
|
|
115
|
+
ast.Name: {"id": str},
|
|
116
|
+
ast.List: {"elts": list[ast.expr]},
|
|
117
|
+
ast.Tuple: {"elts": list[ast.expr]},
|
|
118
|
+
ast.Slice: {"lower": ast.expr | NoneType, "upper": ast.expr | NoneType, "step": ast.expr | NoneType},
|
|
119
|
+
ast.expr_context: {},
|
|
120
|
+
ast.Load: {},
|
|
121
|
+
ast.Store: {},
|
|
122
|
+
ast.Del: {},
|
|
123
|
+
ast.boolop: {},
|
|
124
|
+
ast.And: {},
|
|
125
|
+
ast.Or: {},
|
|
126
|
+
ast.operator: {},
|
|
127
|
+
ast.Add: {},
|
|
128
|
+
ast.Sub: {},
|
|
129
|
+
ast.Mult: {},
|
|
130
|
+
ast.MatMult: {},
|
|
131
|
+
ast.Div: {},
|
|
132
|
+
ast.Mod: {},
|
|
133
|
+
ast.Pow: {},
|
|
134
|
+
ast.LShift: {},
|
|
135
|
+
ast.RShift: {},
|
|
136
|
+
ast.BitOr: {},
|
|
137
|
+
ast.BitXor: {},
|
|
138
|
+
ast.BitAnd: {},
|
|
139
|
+
ast.FloorDiv: {},
|
|
140
|
+
ast.unaryop: {},
|
|
141
|
+
ast.Invert: {},
|
|
142
|
+
ast.Not: {},
|
|
143
|
+
ast.UAdd: {},
|
|
144
|
+
ast.USub: {},
|
|
145
|
+
ast.cmpop: {},
|
|
146
|
+
ast.Eq: {},
|
|
147
|
+
ast.NotEq: {},
|
|
148
|
+
ast.Lt: {},
|
|
149
|
+
ast.LtE: {},
|
|
150
|
+
ast.Gt: {},
|
|
151
|
+
ast.GtE: {},
|
|
152
|
+
ast.Is: {},
|
|
153
|
+
ast.IsNot: {},
|
|
154
|
+
ast.In: {},
|
|
155
|
+
ast.NotIn: {},
|
|
156
|
+
ast.comprehension: {"target": ast.expr, "iter": ast.expr, "ifs": list[ast.expr], "is_async": int},
|
|
157
|
+
ast.excepthandler: {},
|
|
158
|
+
ast.ExceptHandler: {"type": ast.expr | NoneType, "name": str | NoneType, "body": list[ast.stmt]},
|
|
159
|
+
ast.arguments: {
|
|
160
|
+
"posonlyargs": list[ast.arg],
|
|
161
|
+
"args": list[ast.arg],
|
|
162
|
+
"vararg": ast.arg | NoneType,
|
|
163
|
+
"kwonlyargs": list[ast.arg],
|
|
164
|
+
"kw_defaults": list[ast.expr],
|
|
165
|
+
"kwarg": ast.arg | NoneType,
|
|
166
|
+
"defaults": list[ast.expr],
|
|
167
|
+
},
|
|
168
|
+
ast.arg: {"arg": str, "annotation": ast.expr | NoneType, "type_comment": str | NoneType},
|
|
169
|
+
ast.keyword: {"arg": str | NoneType, "value": ast.expr},
|
|
170
|
+
ast.alias: {"name": str, "asname": str | NoneType},
|
|
171
|
+
ast.withitem: {"context_expr": ast.expr, "optional_vars": ast.expr | NoneType},
|
|
172
|
+
ast.match_case: {"pattern": ast.pattern, "guard": ast.expr | NoneType, "body": list[ast.stmt]},
|
|
173
|
+
ast.pattern: {},
|
|
174
|
+
ast.MatchValue: {"value": ast.expr},
|
|
175
|
+
ast.MatchSingleton: {"value": object},
|
|
176
|
+
ast.MatchSequence: {"patterns": list[ast.pattern]},
|
|
177
|
+
ast.MatchMapping: {"keys": list[ast.expr], "patterns": list[ast.pattern], "rest": str | NoneType},
|
|
178
|
+
ast.MatchClass: {
|
|
179
|
+
"cls": ast.expr,
|
|
180
|
+
"patterns": list[ast.pattern],
|
|
181
|
+
"kwd_attrs": list[str],
|
|
182
|
+
"kwd_patterns": list[ast.pattern],
|
|
183
|
+
},
|
|
184
|
+
ast.MatchStar: {"name": str | NoneType},
|
|
185
|
+
ast.MatchAs: {"pattern": ast.pattern | NoneType, "name": str | NoneType},
|
|
186
|
+
ast.MatchOr: {"patterns": list[ast.pattern]},
|
|
187
|
+
ast.type_ignore: {},
|
|
188
|
+
ast.TypeIgnore: {"lineno": int, "tag": str},
|
|
189
|
+
ast.type_param: {},
|
|
190
|
+
ast.TypeVar: {"name": str, "bound": ast.expr | NoneType, "default_value": ast.expr | NoneType},
|
|
191
|
+
ast.ParamSpec: {"name": str, "default_value": ast.expr | NoneType},
|
|
192
|
+
ast.TypeVarTuple: {"name": str, "default_value": ast.expr | NoneType},
|
|
193
|
+
ast.slice: {},
|
|
194
|
+
ast.Index: {},
|
|
195
|
+
ast.ExtSlice: {},
|
|
196
|
+
ast.Suite: {},
|
|
197
|
+
ast.AugLoad: {},
|
|
198
|
+
ast.AugStore: {},
|
|
199
|
+
ast.Param: {},
|
|
200
|
+
}
|
|
201
|
+
if sys.version_info >= (3, 14):
|
|
202
|
+
FIELD_MAPS |= {
|
|
203
|
+
ast.Interpolation: {"value": ast.expr, "str": object, "conversion": int, "format_spec": ast.expr | NoneType},
|
|
204
|
+
ast.TemplateStr: {"values": list[ast.expr]},
|
|
205
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import enum
|
|
6
|
+
import inspect
|
|
7
|
+
import textwrap
|
|
8
|
+
import zoneinfo
|
|
9
|
+
from dataclasses import MISSING, dataclass, fields, is_dataclass
|
|
10
|
+
from types import NoneType, UnionType
|
|
11
|
+
from typing import Annotated, Any, TypeVar, Union, cast, get_args, get_origin
|
|
12
|
+
|
|
13
|
+
from typing_extensions import TypeIs
|
|
14
|
+
|
|
15
|
+
from trolskgen import ast_types, core, templates
|
|
16
|
+
|
|
17
|
+
ASTValue = ast.AST | list[ast.stmt] | str
|
|
18
|
+
T = TypeVar("T", bound=ASTValue)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def converter_ast(o: Any, f: core.F) -> ast.AST | None:
|
|
22
|
+
if not isinstance(o, ast.AST):
|
|
23
|
+
return None
|
|
24
|
+
return o
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@core.upcast_expr
|
|
28
|
+
def converter_simple(o: Any, f: core.F) -> ast.AST | None:
|
|
29
|
+
if isinstance(o, int | float | str | bool | NoneType):
|
|
30
|
+
return ast.Constant(value=o)
|
|
31
|
+
if isinstance(o, list):
|
|
32
|
+
return ast.List(elts=[_downcast(ast.expr, f(e)) for e in o])
|
|
33
|
+
if isinstance(o, tuple):
|
|
34
|
+
return ast.Tuple(elts=[_downcast(ast.expr, f(e)) for e in o])
|
|
35
|
+
if isinstance(o, dict):
|
|
36
|
+
return ast.Dict(
|
|
37
|
+
keys=[_downcast(ast.expr, f(k)) for k in o.keys()],
|
|
38
|
+
values=[_downcast(ast.expr, f(v)) for v in o.values()],
|
|
39
|
+
)
|
|
40
|
+
if isinstance(o, set):
|
|
41
|
+
return ast.Set(elts=[_downcast(ast.expr, f(e)) for e in o])
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
DONT_PREFIX_WITH_MODULE = {"builtins", "typing"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@core.upcast_expr
|
|
49
|
+
def converter_types_and_functions(o: Any, f: core.F) -> ast.AST | None:
|
|
50
|
+
if isinstance(o, type) or inspect.isfunction(o):
|
|
51
|
+
module_name = o.__module__ + "."
|
|
52
|
+
if o.__module__ in DONT_PREFIX_WITH_MODULE:
|
|
53
|
+
module_name = ""
|
|
54
|
+
return f(templates.t(module_name + o.__qualname__))
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FieldMissing: ...
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
FIELD_MISSING = FieldMissing()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@core.upcast_expr
|
|
65
|
+
def converter_common(o: Any, f: core.F) -> ast.AST | None:
|
|
66
|
+
if o is dt.UTC:
|
|
67
|
+
return f(templates.t("dt.UTC"))
|
|
68
|
+
if isinstance(o, zoneinfo.ZoneInfo):
|
|
69
|
+
return f(templates.t("zoneinfo.ZoneInfo({key})", key=o.key))
|
|
70
|
+
if isinstance(o, dt.datetime):
|
|
71
|
+
parts: list[int | ast.AST] = [o.year, o.month, o.day]
|
|
72
|
+
if o.hour or o.minute or o.second or o.microsecond:
|
|
73
|
+
parts.append(o.hour)
|
|
74
|
+
parts.append(o.minute)
|
|
75
|
+
parts.append(o.second)
|
|
76
|
+
if o.microsecond:
|
|
77
|
+
parts.append(o.microsecond)
|
|
78
|
+
if o.tzinfo is not None:
|
|
79
|
+
parts.append(f(templates.t("tzinfo={tzinfo}", tzinfo=o.tzinfo)))
|
|
80
|
+
return f(templates.t("dt.datetime({parts:*})", parts=parts))
|
|
81
|
+
if isinstance(o, dt.date):
|
|
82
|
+
return f(templates.t("dt.date({year}, {month}, {day})", year=o.year, month=o.month, day=o.day))
|
|
83
|
+
if isinstance(o, enum.Enum):
|
|
84
|
+
return f(templates.t("{enum}.{name}", enum=type(o), name=templates.t(o.name)))
|
|
85
|
+
if is_dataclass(o):
|
|
86
|
+
args = list[ast.AST]()
|
|
87
|
+
for field in fields(o):
|
|
88
|
+
value = getattr(o, field.name, FIELD_MISSING)
|
|
89
|
+
if value == field.default:
|
|
90
|
+
continue
|
|
91
|
+
if field.default_factory != MISSING and value == field.default_factory():
|
|
92
|
+
continue
|
|
93
|
+
args.append(f(templates.t("{key}={value}", key=templates.t(field.name), value=value)))
|
|
94
|
+
return f(templates.t("{c}({args:*})", c=type(o), args=args))
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@core.upcast_expr
|
|
99
|
+
def converter_pydantic(o: Any, f: core.F) -> ast.AST | None:
|
|
100
|
+
import pydantic
|
|
101
|
+
|
|
102
|
+
if not isinstance(o, pydantic.BaseModel):
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
args = list[ast.AST]()
|
|
106
|
+
for name, field in o.__class__.model_fields.items():
|
|
107
|
+
value = getattr(o, name, FIELD_MISSING)
|
|
108
|
+
if value == field.default:
|
|
109
|
+
continue
|
|
110
|
+
if field.default_factory and value == field.default_factory(): # type: ignore[call-arg]
|
|
111
|
+
continue
|
|
112
|
+
args.append(f(templates.t("{key}={value}", key=templates.t(name), value=value)))
|
|
113
|
+
return f(templates.t("{c}({args:*})", c=type(o), args=args))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@core.upcast_expr
|
|
117
|
+
def converter_typeform(o: Any, f: core.F) -> ast.AST | None:
|
|
118
|
+
if o is Annotated:
|
|
119
|
+
return f(templates.t("Annotated"))
|
|
120
|
+
if (origin := get_origin(o)) is not None:
|
|
121
|
+
if origin is Union or origin is UnionType:
|
|
122
|
+
a, b, *rest = get_args(o)
|
|
123
|
+
union = f(templates.t("{a} | {b}", a=a, b=b))
|
|
124
|
+
while rest:
|
|
125
|
+
b, *rest = rest
|
|
126
|
+
union = f(templates.t("{a} | {b}", a=union, b=f(b)))
|
|
127
|
+
return union
|
|
128
|
+
slice = [f(arg) for arg in get_args(o)]
|
|
129
|
+
return f(templates.t("{a}[{b:*}]", a=origin, b=slice))
|
|
130
|
+
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def converter_template(o: Any, f: core.F) -> ast.AST | None:
|
|
135
|
+
if not isinstance(o, templates.TemplateLike):
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
o = templates.Template.from_templatelike(o)
|
|
139
|
+
parts = list[str]()
|
|
140
|
+
map = NameNodeMap()
|
|
141
|
+
separator: str = ", "
|
|
142
|
+
for part in o.parts:
|
|
143
|
+
if isinstance(part, str):
|
|
144
|
+
parts.append(part)
|
|
145
|
+
separator = _trailing_indent(part)
|
|
146
|
+
else:
|
|
147
|
+
assert isinstance(part, templates.Interpolation)
|
|
148
|
+
if part.format_spec == "*":
|
|
149
|
+
if not isinstance(part.value, list):
|
|
150
|
+
raise core.TrolskgenError(f"Can only splat lists of values, not: {part.value!r}")
|
|
151
|
+
v = [f(n) for n in part.value]
|
|
152
|
+
else:
|
|
153
|
+
v = [f(part.value)]
|
|
154
|
+
|
|
155
|
+
for i, u in enumerate(v):
|
|
156
|
+
if (named := _named(u)) is None:
|
|
157
|
+
name = map.insert(u)
|
|
158
|
+
parts.append(name)
|
|
159
|
+
else:
|
|
160
|
+
parts.append(named.name)
|
|
161
|
+
if named.annotation is not None:
|
|
162
|
+
name = map.insert(named.annotation)
|
|
163
|
+
parts.append(f": {name}")
|
|
164
|
+
if named.value is not None:
|
|
165
|
+
name = map.insert(named.value)
|
|
166
|
+
parts.append(f" = {name}")
|
|
167
|
+
if i != len(v) - 1:
|
|
168
|
+
parts.append(separator)
|
|
169
|
+
|
|
170
|
+
node_template = textwrap.dedent("".join(parts)).strip()
|
|
171
|
+
try:
|
|
172
|
+
node = ast.parse(node_template)
|
|
173
|
+
except SyntaxError as e:
|
|
174
|
+
raise core.TrolskgenError(f"SyntaxError with template:\n\n{node_template}") from e
|
|
175
|
+
|
|
176
|
+
_ast_replace(node, map)
|
|
177
|
+
assert not map.map
|
|
178
|
+
return node
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# `converter_template` replacement helpers
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class NameNodeMap:
|
|
185
|
+
def __init__(self) -> None:
|
|
186
|
+
self.i = -1
|
|
187
|
+
self.map = dict[str, ast.AST]()
|
|
188
|
+
|
|
189
|
+
def insert(self, v: ast.AST) -> str:
|
|
190
|
+
self.i += 1
|
|
191
|
+
name = f"__tg_{self.i}"
|
|
192
|
+
self.map[name] = v
|
|
193
|
+
return name
|
|
194
|
+
|
|
195
|
+
def pop(self, v: ast.AST | str) -> ast.AST | None:
|
|
196
|
+
if isinstance(v, ast.Expr):
|
|
197
|
+
v = v.value
|
|
198
|
+
if isinstance(v, ast.Name):
|
|
199
|
+
v = v.id
|
|
200
|
+
if isinstance(v, str) and v in self.map:
|
|
201
|
+
return self.map.pop(v)
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _is_instance(v: ASTValue, t: type[T]) -> TypeIs[T]:
|
|
206
|
+
"""`isinstance`, but handles `t = list[T], t = A | B`"""
|
|
207
|
+
if get_origin(t) is list:
|
|
208
|
+
[t_inner] = get_args(t)
|
|
209
|
+
return isinstance(v, list) and all(_is_instance(u, t_inner) for u in v)
|
|
210
|
+
if get_origin(t) is Union:
|
|
211
|
+
return any(_is_instance(v, t_inner) for t_inner in get_args(t))
|
|
212
|
+
|
|
213
|
+
return isinstance(v, t)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _downcast(t: type[T], v: ASTValue) -> T:
|
|
217
|
+
"""Keep removing layers of AST until hopefully, `_is_instance(v, t)`."""
|
|
218
|
+
v_original = v
|
|
219
|
+
while not _is_instance(v, t):
|
|
220
|
+
if isinstance(v, list) and get_origin(t) is list:
|
|
221
|
+
[r] = get_args(t)
|
|
222
|
+
return cast(T, [_downcast(r, u) for u in v])
|
|
223
|
+
if isinstance(v, ast.Module):
|
|
224
|
+
v = v.body
|
|
225
|
+
elif isinstance(v, list) and len(v) == 1:
|
|
226
|
+
v = v[0]
|
|
227
|
+
elif isinstance(v, ast.Expr):
|
|
228
|
+
v = v.value
|
|
229
|
+
elif isinstance(v, ast.Name):
|
|
230
|
+
v = v.id
|
|
231
|
+
else:
|
|
232
|
+
raise core.TrolskgenError(f"Cannot convert {v_original!r} to {t!r}, got as far as {v!r}")
|
|
233
|
+
return v
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass
|
|
237
|
+
class _Named:
|
|
238
|
+
name: str
|
|
239
|
+
annotation: ast.expr | None
|
|
240
|
+
value: ast.expr | None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _named(v: ast.AST) -> _Named | None:
|
|
244
|
+
"""Convert eg. `a: int = 1` to `Named("a", int, 1)`."""
|
|
245
|
+
if isinstance(v, ast.Module) and len(v.body) == 1:
|
|
246
|
+
v = v.body[0]
|
|
247
|
+
if isinstance(v, ast.Assign) and len(v.targets) == 1 and isinstance(v.targets[0], ast.Name):
|
|
248
|
+
return _Named(v.targets[0].id, None, v.value)
|
|
249
|
+
if isinstance(v, ast.AnnAssign) and isinstance(v.target, ast.Name):
|
|
250
|
+
return _Named(v.target.id, v.annotation, v.value)
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _trailing_indent(part: str) -> str:
|
|
255
|
+
"""Get the trailing indent of a part.
|
|
256
|
+
|
|
257
|
+
If previous to the newline, there was a comma, assume we're in a
|
|
258
|
+
comma separated list.
|
|
259
|
+
"""
|
|
260
|
+
lines = part.splitlines()
|
|
261
|
+
try:
|
|
262
|
+
trailing_comma = lines[-2][-1] == ","
|
|
263
|
+
except IndexError:
|
|
264
|
+
trailing_comma = False
|
|
265
|
+
if all(c == " " for c in lines[-1]) and not trailing_comma:
|
|
266
|
+
return "\n" + lines[-1]
|
|
267
|
+
return ", "
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _ast_replace(node: ast.AST, map: NameNodeMap) -> None:
|
|
271
|
+
"""Traverse the AST, replacing nodes with items from the `map`."""
|
|
272
|
+
type_map = ast_types.FIELD_MAPS.get(type(node))
|
|
273
|
+
if not type_map:
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
for name, t in type_map.items():
|
|
277
|
+
v = getattr(node, name)
|
|
278
|
+
|
|
279
|
+
if (interpolation := map.pop(v)) is not None:
|
|
280
|
+
v = interpolation
|
|
281
|
+
elif isinstance(v, list):
|
|
282
|
+
for i, u in enumerate(v):
|
|
283
|
+
if (interpolation := map.pop(u)) is not None:
|
|
284
|
+
v[i] = interpolation
|
|
285
|
+
|
|
286
|
+
v = _downcast(t, v)
|
|
287
|
+
setattr(node, name, v)
|
|
288
|
+
|
|
289
|
+
# recurse
|
|
290
|
+
if isinstance(v, list):
|
|
291
|
+
for u in v:
|
|
292
|
+
_ast_replace(u, map)
|
|
293
|
+
else:
|
|
294
|
+
_ast_replace(v, map)
|