auto-editor 27.1.1__py3-none-any.whl → 28.0.1__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.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +0 -8
- auto_editor/cmds/cache.py +1 -1
- auto_editor/cmds/desc.py +8 -9
- auto_editor/cmds/info.py +4 -17
- auto_editor/cmds/palet.py +1 -1
- auto_editor/cmds/subdump.py +2 -4
- auto_editor/cmds/test.py +50 -31
- auto_editor/edit.py +52 -52
- auto_editor/{formats → exports}/fcp11.py +11 -7
- auto_editor/{formats → exports}/fcp7.py +7 -239
- auto_editor/exports/json.py +32 -0
- auto_editor/{formats → exports}/shotcut.py +8 -17
- auto_editor/ffwrapper.py +1 -3
- auto_editor/help.py +6 -17
- auto_editor/imports/__init__.py +0 -0
- auto_editor/imports/fcp7.py +275 -0
- auto_editor/{formats → imports}/json.py +16 -43
- auto_editor/json.py +2 -2
- auto_editor/lang/palet.py +6 -43
- auto_editor/lang/stdenv.py +8 -20
- auto_editor/lib/contracts.py +4 -6
- auto_editor/lib/data_structs.py +0 -3
- auto_editor/make_layers.py +13 -13
- auto_editor/preview.py +1 -2
- auto_editor/render/audio.py +9 -6
- auto_editor/render/video.py +3 -2
- auto_editor/timeline.py +35 -52
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/METADATA +2 -2
- auto_editor-28.0.1.dist-info/RECORD +56 -0
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/WHEEL +1 -1
- auto_editor/formats/utils.py +0 -56
- auto_editor-27.1.1.dist-info/RECORD +0 -54
- /auto_editor/{formats → exports}/__init__.py +0 -0
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/entry_points.txt +0 -0
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/licenses/LICENSE +0 -0
- {auto_editor-27.1.1.dist-info → auto_editor-28.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,275 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import xml.etree.ElementTree as ET
|
4
|
+
from fractions import Fraction
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
from urllib.parse import unquote
|
7
|
+
from xml.etree.ElementTree import Element
|
8
|
+
|
9
|
+
from auto_editor.ffwrapper import FileInfo
|
10
|
+
from auto_editor.timeline import ASpace, Clip, Template, VSpace, v3
|
11
|
+
|
12
|
+
if TYPE_CHECKING:
|
13
|
+
from auto_editor.utils.log import Log
|
14
|
+
|
15
|
+
|
16
|
+
SUPPORTED_EFFECTS = ("timeremap",)
|
17
|
+
|
18
|
+
|
19
|
+
def show(ele: Element, limit: int, depth: int = 0) -> None:
|
20
|
+
print(
|
21
|
+
f"{' ' * (depth * 4)}<{ele.tag} {ele.attrib}> {ele.text.strip() if ele.text is not None else ''}"
|
22
|
+
)
|
23
|
+
for child in ele:
|
24
|
+
if isinstance(child, Element) and depth < limit:
|
25
|
+
show(child, limit, depth + 1)
|
26
|
+
|
27
|
+
|
28
|
+
def read_filters(clipitem: Element, log: Log) -> float:
|
29
|
+
for effect_tag in clipitem:
|
30
|
+
if effect_tag.tag in {"enabled", "start", "end"}:
|
31
|
+
continue
|
32
|
+
if len(effect_tag) < 3:
|
33
|
+
log.error("<effect> requires: <effectid> <name> and one <parameter>")
|
34
|
+
for i, effects in enumerate(effect_tag):
|
35
|
+
if i == 0 and effects.tag != "name":
|
36
|
+
log.error("<effect>: <name> must be first tag")
|
37
|
+
if i == 1 and effects.tag != "effectid":
|
38
|
+
log.error("<effect>: <effectid> must be second tag")
|
39
|
+
if effects.text not in SUPPORTED_EFFECTS:
|
40
|
+
log.error(f"`{effects.text}` is not a supported effect.")
|
41
|
+
|
42
|
+
if i > 1:
|
43
|
+
for j, parms in enumerate(effects):
|
44
|
+
if j == 0:
|
45
|
+
if parms.tag != "parameterid":
|
46
|
+
log.error("<parameter>: <parameterid> must be first tag")
|
47
|
+
if parms.text != "speed":
|
48
|
+
break
|
49
|
+
|
50
|
+
if j > 0 and parms.tag == "value":
|
51
|
+
if parms.text is None:
|
52
|
+
log.error("<value>: number required")
|
53
|
+
return float(parms.text) / 100
|
54
|
+
|
55
|
+
return 1.0
|
56
|
+
|
57
|
+
|
58
|
+
def uri_to_path(uri: str) -> str:
|
59
|
+
# Handle inputs like:
|
60
|
+
# /Users/wyattblue/projects/auto-editor/example.mp4
|
61
|
+
# file:///Users/wyattblue/projects/auto-editor/example.mp4
|
62
|
+
# file:///C:/Users/WyattBlue/projects/auto-editor/example.mp4
|
63
|
+
# file://localhost/Users/wyattblue/projects/auto-editor/example.mp4
|
64
|
+
|
65
|
+
if uri.startswith("file://localhost/"):
|
66
|
+
uri = uri[16:]
|
67
|
+
elif uri.startswith("file://"):
|
68
|
+
# Windows-style paths
|
69
|
+
uri = uri[8:] if len(uri) > 8 and uri[9] == ":" else uri[7:]
|
70
|
+
else:
|
71
|
+
return uri
|
72
|
+
return unquote(uri)
|
73
|
+
|
74
|
+
|
75
|
+
def read_tb_ntsc(tb: int, ntsc: bool) -> Fraction:
|
76
|
+
if ntsc:
|
77
|
+
if tb == 24:
|
78
|
+
return Fraction(24000, 1001)
|
79
|
+
if tb == 30:
|
80
|
+
return Fraction(30000, 1001)
|
81
|
+
if tb == 60:
|
82
|
+
return Fraction(60000, 1001)
|
83
|
+
return tb * Fraction(999, 1000)
|
84
|
+
|
85
|
+
return Fraction(tb)
|
86
|
+
|
87
|
+
|
88
|
+
def fcp7_read_xml(path: str, log: Log) -> v3:
|
89
|
+
def xml_bool(val: str) -> bool:
|
90
|
+
if val == "TRUE":
|
91
|
+
return True
|
92
|
+
if val == "FALSE":
|
93
|
+
return False
|
94
|
+
raise TypeError("Value must be 'TRUE' or 'FALSE'")
|
95
|
+
|
96
|
+
try:
|
97
|
+
tree = ET.parse(path)
|
98
|
+
except FileNotFoundError:
|
99
|
+
log.error(f"Could not find '{path}'")
|
100
|
+
|
101
|
+
root = tree.getroot()
|
102
|
+
|
103
|
+
def parse(ele: Element, schema: dict) -> dict:
|
104
|
+
new: dict = {}
|
105
|
+
for key, val in schema.items():
|
106
|
+
if isinstance(val, dict) and "__arr" in val:
|
107
|
+
new[key] = []
|
108
|
+
|
109
|
+
is_arr = False
|
110
|
+
for child in ele:
|
111
|
+
if child.tag not in schema:
|
112
|
+
continue
|
113
|
+
|
114
|
+
if schema[child.tag] is None:
|
115
|
+
new[child.tag] = child
|
116
|
+
continue
|
117
|
+
|
118
|
+
if isinstance(schema[child.tag], dict):
|
119
|
+
val = parse(child, schema[child.tag])
|
120
|
+
is_arr = "__arr" in schema[child.tag]
|
121
|
+
else:
|
122
|
+
val = schema[child.tag](child.text)
|
123
|
+
|
124
|
+
if child.tag in new:
|
125
|
+
if not is_arr:
|
126
|
+
log.error(f"<{child.tag}> can only occur once")
|
127
|
+
new[child.tag].append(val)
|
128
|
+
else:
|
129
|
+
new[child.tag] = [val] if is_arr else val
|
130
|
+
|
131
|
+
return new
|
132
|
+
|
133
|
+
def check(ele: Element, tag: str) -> None:
|
134
|
+
if tag != ele.tag:
|
135
|
+
log.error(f"Expected '{tag}' tag, got '{ele.tag}'")
|
136
|
+
|
137
|
+
check(root, "xmeml")
|
138
|
+
check(root[0], "sequence")
|
139
|
+
result = parse(
|
140
|
+
root[0],
|
141
|
+
{
|
142
|
+
"name": str,
|
143
|
+
"duration": int,
|
144
|
+
"rate": {
|
145
|
+
"timebase": Fraction,
|
146
|
+
"ntsc": xml_bool,
|
147
|
+
},
|
148
|
+
"media": None,
|
149
|
+
},
|
150
|
+
)
|
151
|
+
|
152
|
+
tb = read_tb_ntsc(result["rate"]["timebase"], result["rate"]["ntsc"])
|
153
|
+
av = parse(
|
154
|
+
result["media"],
|
155
|
+
{
|
156
|
+
"video": None,
|
157
|
+
"audio": None,
|
158
|
+
},
|
159
|
+
)
|
160
|
+
|
161
|
+
sources: dict[str, FileInfo] = {}
|
162
|
+
vobjs: VSpace = []
|
163
|
+
aobjs: ASpace = []
|
164
|
+
|
165
|
+
vclip_schema = {
|
166
|
+
"format": {
|
167
|
+
"samplecharacteristics": {
|
168
|
+
"width": int,
|
169
|
+
"height": int,
|
170
|
+
},
|
171
|
+
},
|
172
|
+
"track": {
|
173
|
+
"__arr": "",
|
174
|
+
"clipitem": {
|
175
|
+
"__arr": "",
|
176
|
+
"start": int,
|
177
|
+
"end": int,
|
178
|
+
"in": int,
|
179
|
+
"out": int,
|
180
|
+
"file": None,
|
181
|
+
"filter": None,
|
182
|
+
},
|
183
|
+
},
|
184
|
+
}
|
185
|
+
|
186
|
+
aclip_schema = {
|
187
|
+
"format": {"samplecharacteristics": {"samplerate": int}},
|
188
|
+
"track": {
|
189
|
+
"__arr": "",
|
190
|
+
"clipitem": {
|
191
|
+
"__arr": "",
|
192
|
+
"start": int,
|
193
|
+
"end": int,
|
194
|
+
"in": int,
|
195
|
+
"out": int,
|
196
|
+
"file": None,
|
197
|
+
"filter": None,
|
198
|
+
},
|
199
|
+
},
|
200
|
+
}
|
201
|
+
|
202
|
+
sr = 48000
|
203
|
+
res = (1920, 1080)
|
204
|
+
|
205
|
+
if "video" in av:
|
206
|
+
tracks = parse(av["video"], vclip_schema)
|
207
|
+
|
208
|
+
if "format" in tracks:
|
209
|
+
width = tracks["format"]["samplecharacteristics"]["width"]
|
210
|
+
height = tracks["format"]["samplecharacteristics"]["height"]
|
211
|
+
res = width, height
|
212
|
+
|
213
|
+
for t, track in enumerate(tracks["track"]):
|
214
|
+
if len(track["clipitem"]) > 0:
|
215
|
+
vobjs.append([])
|
216
|
+
for clipitem in track["clipitem"]:
|
217
|
+
file_id = clipitem["file"].attrib["id"]
|
218
|
+
if file_id not in sources:
|
219
|
+
fileobj = parse(clipitem["file"], {"pathurl": str})
|
220
|
+
|
221
|
+
if "pathurl" in fileobj:
|
222
|
+
sources[file_id] = FileInfo.init(
|
223
|
+
uri_to_path(fileobj["pathurl"]),
|
224
|
+
log,
|
225
|
+
)
|
226
|
+
else:
|
227
|
+
show(clipitem["file"], 3)
|
228
|
+
log.error(
|
229
|
+
f"'pathurl' child element not found in {clipitem['file'].tag}"
|
230
|
+
)
|
231
|
+
|
232
|
+
if "filter" in clipitem:
|
233
|
+
speed = read_filters(clipitem["filter"], log)
|
234
|
+
else:
|
235
|
+
speed = 1.0
|
236
|
+
|
237
|
+
start = clipitem["start"]
|
238
|
+
dur = clipitem["end"] - start
|
239
|
+
offset = clipitem["in"]
|
240
|
+
|
241
|
+
vobjs[t].append(
|
242
|
+
Clip(start, dur, sources[file_id], offset, stream=0, speed=speed)
|
243
|
+
)
|
244
|
+
|
245
|
+
if "audio" in av:
|
246
|
+
tracks = parse(av["audio"], aclip_schema)
|
247
|
+
if "format" in tracks:
|
248
|
+
sr = tracks["format"]["samplecharacteristics"]["samplerate"]
|
249
|
+
|
250
|
+
for t, track in enumerate(tracks["track"]):
|
251
|
+
if len(track["clipitem"]) > 0:
|
252
|
+
aobjs.append([])
|
253
|
+
for clipitem in track["clipitem"]:
|
254
|
+
file_id = clipitem["file"].attrib["id"]
|
255
|
+
if file_id not in sources:
|
256
|
+
fileobj = parse(clipitem["file"], {"pathurl": str})
|
257
|
+
sources[file_id] = FileInfo.init(
|
258
|
+
uri_to_path(fileobj["pathurl"]), log
|
259
|
+
)
|
260
|
+
|
261
|
+
if "filter" in clipitem:
|
262
|
+
speed = read_filters(clipitem["filter"], log)
|
263
|
+
else:
|
264
|
+
speed = 1.0
|
265
|
+
|
266
|
+
start = clipitem["start"]
|
267
|
+
dur = clipitem["end"] - start
|
268
|
+
offset = clipitem["in"]
|
269
|
+
|
270
|
+
aobjs[t].append(
|
271
|
+
Clip(start, dur, sources[file_id], offset, stream=0, speed=speed)
|
272
|
+
)
|
273
|
+
|
274
|
+
T = Template.init(sources[next(iter(sources))], sr, res=res)
|
275
|
+
return v3(tb, "#000", T, vobjs, aobjs, v1=None)
|
@@ -1,32 +1,27 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import os
|
4
|
-
import sys
|
5
4
|
from difflib import get_close_matches
|
6
5
|
from fractions import Fraction
|
7
|
-
from typing import Any
|
6
|
+
from typing import TYPE_CHECKING, Any
|
8
7
|
|
9
8
|
from auto_editor.ffwrapper import FileInfo
|
10
|
-
from auto_editor.json import
|
9
|
+
from auto_editor.json import load
|
11
10
|
from auto_editor.lib.err import MyError
|
12
11
|
from auto_editor.timeline import (
|
13
|
-
|
12
|
+
Clip,
|
14
13
|
Template,
|
15
|
-
TlAudio,
|
16
|
-
TlVideo,
|
17
|
-
VSpace,
|
18
14
|
audio_builder,
|
19
15
|
v1,
|
20
16
|
v3,
|
21
17
|
visual_objects,
|
22
18
|
)
|
23
19
|
from auto_editor.utils.cmdkw import ParserError, Required, pAttrs
|
24
|
-
from auto_editor.utils.log import Log
|
25
20
|
from auto_editor.utils.types import CoerceError
|
26
21
|
|
27
|
-
|
28
|
-
|
29
|
-
|
22
|
+
if TYPE_CHECKING:
|
23
|
+
from auto_editor.timeline import ASpace, VSpace
|
24
|
+
from auto_editor.utils.log import Log
|
30
25
|
|
31
26
|
|
32
27
|
def check_attrs(data: object, log: Log, *attrs: str) -> None:
|
@@ -116,7 +111,7 @@ def read_v3(tl: Any, log: Log) -> v3:
|
|
116
111
|
tb = Fraction(tl["timebase"])
|
117
112
|
|
118
113
|
v: Any = []
|
119
|
-
a: list[list[
|
114
|
+
a: list[list[Clip]] = []
|
120
115
|
|
121
116
|
for vlayers in tl["v"]:
|
122
117
|
if vlayers:
|
@@ -145,7 +140,7 @@ def read_v3(tl: Any, log: Log) -> v3:
|
|
145
140
|
log.error(f"Unknown audio object: {adict['name']}")
|
146
141
|
|
147
142
|
try:
|
148
|
-
a_out.append(
|
143
|
+
a_out.append(Clip(**parse_obj(adict, audio_builder)))
|
149
144
|
except ParserError as e:
|
150
145
|
log.error(e)
|
151
146
|
|
@@ -202,10 +197,10 @@ def read_v1(tl: Any, log: Log) -> v3:
|
|
202
197
|
if src.videos:
|
203
198
|
if len(vtl) == 0:
|
204
199
|
vtl.append([])
|
205
|
-
vtl[0].append(
|
200
|
+
vtl[0].append(Clip(c.start, c.dur, c.src, c.offset, 0, c.speed))
|
206
201
|
|
207
202
|
for a in range(len(src.audios)):
|
208
|
-
atl[a].append(
|
203
|
+
atl[a].append(Clip(c.start, c.dur, c.src, c.offset, a, c.speed))
|
209
204
|
|
210
205
|
return v3(
|
211
206
|
src.get_fps(),
|
@@ -218,11 +213,13 @@ def read_v1(tl: Any, log: Log) -> v3:
|
|
218
213
|
|
219
214
|
|
220
215
|
def read_json(path: str, log: Log) -> v3:
|
221
|
-
|
222
|
-
|
216
|
+
try:
|
217
|
+
with open(path, encoding="utf-8", errors="ignore") as f:
|
223
218
|
tl = load(path, f)
|
224
|
-
|
225
|
-
|
219
|
+
except FileNotFoundError:
|
220
|
+
log.error(f"File not found: {path}")
|
221
|
+
except MyError as e:
|
222
|
+
log.error(e)
|
226
223
|
|
227
224
|
check_attrs(tl, log, "version")
|
228
225
|
|
@@ -235,27 +232,3 @@ def read_json(path: str, log: Log) -> v3:
|
|
235
232
|
if type(ver) is not str:
|
236
233
|
log.error("version needs to be a string")
|
237
234
|
log.error(f"Importing version {ver} timelines is not supported.")
|
238
|
-
|
239
|
-
|
240
|
-
def make_json_timeline(ver: int, out: str | int, tl: v3, log: Log) -> None:
|
241
|
-
if ver not in {3, 1}:
|
242
|
-
log.error(f"Version {ver} is not supported!")
|
243
|
-
|
244
|
-
if isinstance(out, str):
|
245
|
-
if not out.endswith(".json"):
|
246
|
-
log.error("Output extension must be .json")
|
247
|
-
outfile: Any = open(out, "w")
|
248
|
-
else:
|
249
|
-
outfile = sys.stdout
|
250
|
-
|
251
|
-
if ver == 3:
|
252
|
-
dump(tl.as_dict(), outfile, indent=2)
|
253
|
-
else:
|
254
|
-
if tl.v1 is None:
|
255
|
-
log.error("Timeline can't be converted to v1 format")
|
256
|
-
dump(tl.v1.as_dict(), outfile, indent=2)
|
257
|
-
|
258
|
-
if isinstance(out, str):
|
259
|
-
outfile.close()
|
260
|
-
else:
|
261
|
-
print("") # Flush stdout
|
auto_editor/json.py
CHANGED
@@ -89,7 +89,7 @@ class Lexer:
|
|
89
89
|
|
90
90
|
if self.char == "u":
|
91
91
|
buf = ""
|
92
|
-
for
|
92
|
+
for _ in range(4):
|
93
93
|
self.advance()
|
94
94
|
if self.char is None:
|
95
95
|
self.error("\\u escape sequence needs 4 hexs")
|
@@ -162,7 +162,7 @@ class Lexer:
|
|
162
162
|
return (key, None)
|
163
163
|
|
164
164
|
keyword = ""
|
165
|
-
for
|
165
|
+
for _ in range(5): # Longest valid keyword length
|
166
166
|
if self.char is None or self.char in " \t\n\r\x0b\x0c[]}{,":
|
167
167
|
break
|
168
168
|
keyword += self.char
|
auto_editor/lang/palet.py
CHANGED
@@ -65,20 +65,11 @@ class Token:
|
|
65
65
|
|
66
66
|
|
67
67
|
class Lexer:
|
68
|
-
__slots__ = (
|
69
|
-
|
70
|
-
|
71
|
-
"allow_lang_prag",
|
72
|
-
"pos",
|
73
|
-
"char",
|
74
|
-
"lineno",
|
75
|
-
"column",
|
76
|
-
)
|
77
|
-
|
78
|
-
def __init__(self, filename: str, text: str, langprag: bool = False):
|
68
|
+
__slots__ = ("filename", "text", "pos", "char", "lineno", "column")
|
69
|
+
|
70
|
+
def __init__(self, filename: str, text: str):
|
79
71
|
self.filename = filename
|
80
72
|
self.text = text
|
81
|
-
self.allow_lang_prag = langprag
|
82
73
|
self.pos: int = 0
|
83
74
|
self.lineno: int = 1
|
84
75
|
self.column: int = 1
|
@@ -157,7 +148,7 @@ class Lexer:
|
|
157
148
|
token = SEC
|
158
149
|
elif unit == "dB":
|
159
150
|
token = DB
|
160
|
-
elif unit != "
|
151
|
+
elif unit != "%":
|
161
152
|
return Token(
|
162
153
|
VAL,
|
163
154
|
Sym(result + unit, self.lineno, self.column),
|
@@ -166,9 +157,7 @@ class Lexer:
|
|
166
157
|
)
|
167
158
|
|
168
159
|
try:
|
169
|
-
if unit == "
|
170
|
-
return Token(VAL, complex(result + "j"), self.lineno, self.column)
|
171
|
-
elif unit == "%":
|
160
|
+
if unit == "%":
|
172
161
|
return Token(VAL, float(result) / 100, self.lineno, self.column)
|
173
162
|
elif "/" in result:
|
174
163
|
return Token(token, Fraction(result), self.lineno, self.column)
|
@@ -289,32 +278,6 @@ class Lexer:
|
|
289
278
|
self.advance()
|
290
279
|
if self.char is None or self.char == "\n":
|
291
280
|
continue
|
292
|
-
|
293
|
-
elif self.char == "l" and self.peek() == "a":
|
294
|
-
buf = StringIO()
|
295
|
-
while self.char_is_norm():
|
296
|
-
assert self.char is not None
|
297
|
-
buf.write(self.char)
|
298
|
-
self.advance()
|
299
|
-
|
300
|
-
result = buf.getvalue()
|
301
|
-
if result != "lang":
|
302
|
-
self.error(f"Unknown hash literal `#{result}`")
|
303
|
-
if not self.allow_lang_prag:
|
304
|
-
self.error("#lang pragma is not allowed here")
|
305
|
-
|
306
|
-
self.advance()
|
307
|
-
buf = StringIO()
|
308
|
-
while not self.is_whitespace():
|
309
|
-
assert self.char is not None
|
310
|
-
buf.write(self.char)
|
311
|
-
self.advance()
|
312
|
-
|
313
|
-
result = buf.getvalue()
|
314
|
-
if result != "palet":
|
315
|
-
self.error(f"Invalid #lang: {result}")
|
316
|
-
self.allow_lang_prag = False
|
317
|
-
continue
|
318
281
|
else:
|
319
282
|
return self.hash_literal()
|
320
283
|
|
@@ -747,7 +710,7 @@ env.update({
|
|
747
710
|
# fmt: on
|
748
711
|
|
749
712
|
|
750
|
-
def interpret(env: Env, parser: Parser) -> list:
|
713
|
+
def interpret(env: Env, parser: Parser) -> list[object]:
|
751
714
|
result = []
|
752
715
|
|
753
716
|
try:
|
auto_editor/lang/stdenv.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from dataclasses import dataclass
|
3
4
|
from typing import TYPE_CHECKING
|
4
5
|
|
5
6
|
import bv
|
@@ -7,23 +8,24 @@ import bv
|
|
7
8
|
from auto_editor.analyze import mut_remove_large, mut_remove_small
|
8
9
|
from auto_editor.lib.contracts import *
|
9
10
|
from auto_editor.lib.data_structs import *
|
11
|
+
from auto_editor.lib.err import MyError
|
10
12
|
|
11
13
|
from .palet import Syntax, env, is_boolarr, is_iterable, my_eval, p_slice, raise_, ref
|
12
14
|
|
13
15
|
if TYPE_CHECKING:
|
16
|
+
from fractions import Fraction
|
14
17
|
from typing import Any, Literal
|
15
18
|
|
16
19
|
import numpy as np
|
17
20
|
from numpy.typing import NDArray
|
18
21
|
|
19
|
-
Number = int | float |
|
22
|
+
Number = int | float | Fraction
|
20
23
|
BoolList = NDArray[np.bool_]
|
21
24
|
Node = tuple
|
22
25
|
|
23
26
|
|
24
27
|
def make_standard_env() -> dict[str, Any]:
|
25
28
|
import os.path
|
26
|
-
from cmath import sqrt as complex_sqrt
|
27
29
|
from functools import reduce
|
28
30
|
from operator import add, ge, gt, is_, le, lt, mod, mul
|
29
31
|
from subprocess import run
|
@@ -509,7 +511,7 @@ def make_standard_env() -> dict[str, Any]:
|
|
509
511
|
for c in node[2:]:
|
510
512
|
my_eval(env, c)
|
511
513
|
|
512
|
-
def syn_quote(
|
514
|
+
def syn_quote(_: Env, node: Node) -> Any:
|
513
515
|
guard_term(node, 2, 2)
|
514
516
|
if type(node[1]) is Keyword:
|
515
517
|
return QuotedKeyword(node[1])
|
@@ -825,14 +827,6 @@ def make_standard_env() -> dict[str, Any]:
|
|
825
827
|
|
826
828
|
return reduce(lambda a, b: a // b, m, n)
|
827
829
|
|
828
|
-
def _sqrt(v: Number) -> Number:
|
829
|
-
r = complex_sqrt(v)
|
830
|
-
if r.imag == 0:
|
831
|
-
if int(r.real) == r.real:
|
832
|
-
return int(r.real)
|
833
|
-
return r.real
|
834
|
-
return r
|
835
|
-
|
836
830
|
def _xor(*vals: Any) -> bool | BoolList:
|
837
831
|
if is_boolarr(vals[0]):
|
838
832
|
check_args("xor", vals, (2, None), (is_boolarr,))
|
@@ -841,9 +835,6 @@ def make_standard_env() -> dict[str, Any]:
|
|
841
835
|
return reduce(lambda a, b: a ^ b, vals)
|
842
836
|
|
843
837
|
def number_to_string(val: Number) -> str:
|
844
|
-
if isinstance(val, complex):
|
845
|
-
join = "" if val.imag < 0 else "+"
|
846
|
-
return f"{val.real}{join}{val.imag}i"
|
847
838
|
return f"{val}"
|
848
839
|
|
849
840
|
def string_to_number(val) -> float:
|
@@ -928,7 +919,7 @@ def make_standard_env() -> dict[str, Any]:
|
|
928
919
|
except Exception:
|
929
920
|
raise MyError("hash-ref: invalid key")
|
930
921
|
|
931
|
-
def hash_set(h: dict, k: object, v: object) -> None:
|
922
|
+
def hash_set(h: dict[object, object], k: object, v: object) -> None:
|
932
923
|
h[k] = v
|
933
924
|
|
934
925
|
def hash_remove(h: dict, v: object) -> None:
|
@@ -953,7 +944,7 @@ def make_standard_env() -> dict[str, Any]:
|
|
953
944
|
except Exception:
|
954
945
|
return False
|
955
946
|
|
956
|
-
def change_file_ext(a, ext) -> str:
|
947
|
+
def change_file_ext(a: str, ext: str) -> str:
|
957
948
|
import os.path
|
958
949
|
|
959
950
|
base_name = os.path.splitext(a)[0]
|
@@ -968,6 +959,7 @@ def make_standard_env() -> dict[str, Any]:
|
|
968
959
|
# syntax
|
969
960
|
"lambda": Syntax(syn_lambda),
|
970
961
|
"λ": Syntax(syn_lambda),
|
962
|
+
"defn": Syntax(syn_define),
|
971
963
|
"define": Syntax(syn_define),
|
972
964
|
"define/c": Syntax(syn_definec),
|
973
965
|
"set!": Syntax(syn_set),
|
@@ -994,7 +986,6 @@ def make_standard_env() -> dict[str, Any]:
|
|
994
986
|
"int?": is_int,
|
995
987
|
"float?": is_float,
|
996
988
|
"frac?": is_frac,
|
997
|
-
"complex?": Contract("complex?", lambda v: type(v) is complex),
|
998
989
|
"nat?": is_nat,
|
999
990
|
"nat1?": is_nat1,
|
1000
991
|
"threshold?": is_threshold,
|
@@ -1049,9 +1040,6 @@ def make_standard_env() -> dict[str, Any]:
|
|
1049
1040
|
"div": Proc("div", int_div, (2, None), is_int),
|
1050
1041
|
"add1": Proc("add1", lambda z: z + 1, (1, 1), is_num),
|
1051
1042
|
"sub1": Proc("sub1", lambda z: z - 1, (1, 1), is_num),
|
1052
|
-
"sqrt": Proc("sqrt", _sqrt, (1, 1), is_num),
|
1053
|
-
"real-part": Proc("real-part", lambda v: v.real, (1, 1), is_num),
|
1054
|
-
"imag-part": Proc("imag-part", lambda v: v.imag, (1, 1), is_num),
|
1055
1043
|
# reals
|
1056
1044
|
"pow": Proc("pow", pow, (2, 2), is_real),
|
1057
1045
|
"abs": Proc("abs", abs, (1, 1), is_real),
|
auto_editor/lib/contracts.py
CHANGED
@@ -46,7 +46,7 @@ def check_contract(c: object, val: object) -> bool:
|
|
46
46
|
return val is True
|
47
47
|
if c is False:
|
48
48
|
return val is False
|
49
|
-
if type(c) in (int, float, float64, Fraction,
|
49
|
+
if type(c) in (int, float, float64, Fraction, str, Sym):
|
50
50
|
return val == c
|
51
51
|
raise MyError(f"Invalid contract, got: {print_str(c)}")
|
52
52
|
|
@@ -97,7 +97,7 @@ class Proc:
|
|
97
97
|
self.kw_contracts = None
|
98
98
|
self.contracts = c
|
99
99
|
|
100
|
-
def __call__(self, *args: Any, **kwargs: Any):
|
100
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
101
101
|
lower, upper = self.arity
|
102
102
|
amount = len(args)
|
103
103
|
cont = self.contracts
|
@@ -164,7 +164,7 @@ def is_contract(c: object) -> bool:
|
|
164
164
|
return True
|
165
165
|
if c is True or c is False:
|
166
166
|
return True
|
167
|
-
return type(c) in (int, float, Fraction,
|
167
|
+
return type(c) in (int, float, Fraction, str, Sym)
|
168
168
|
|
169
169
|
|
170
170
|
is_bool = Contract("bool?", lambda v: type(v) is bool)
|
@@ -172,10 +172,8 @@ is_int = Contract("int?", lambda v: type(v) is int)
|
|
172
172
|
is_nat = Contract("nat?", lambda v: type(v) is int and v > -1)
|
173
173
|
is_nat1 = Contract("nat1?", lambda v: type(v) is int and v > 0)
|
174
174
|
int_not_zero = Contract("(or/c (not/c 0) int?)", lambda v: v != 0 and is_int(v))
|
175
|
-
is_num = Contract(
|
176
|
-
"number?", lambda v: type(v) in (int, float, float64, Fraction, complex)
|
177
|
-
)
|
178
175
|
is_real = Contract("real?", lambda v: type(v) in (int, float, float64, Fraction))
|
176
|
+
is_num = is_real
|
179
177
|
is_float = Contract("float?", lambda v: type(v) in (float, float64))
|
180
178
|
is_frac = Contract("frac?", lambda v: type(v) is Fraction)
|
181
179
|
is_str = Contract("string?", lambda v: type(v) is str)
|
auto_editor/lib/data_structs.py
CHANGED
@@ -182,9 +182,6 @@ def display_str(val: object) -> str:
|
|
182
182
|
return f"{val}"
|
183
183
|
if type(val) is range:
|
184
184
|
return "#<range>"
|
185
|
-
if type(val) is complex:
|
186
|
-
join = "" if val.imag < 0 else "+"
|
187
|
-
return f"{val.real}{join}{val.imag}i"
|
188
185
|
if type(val) is np.bool_:
|
189
186
|
return "1" if val else "0"
|
190
187
|
if type(val) is np.float64 or type(val) is np.float32:
|