auto-editor 25.3.1__py3-none-any.whl → 26.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 CHANGED
@@ -1 +1 @@
1
- __version__ = "25.3.1"
1
+ __version__ = "26.0.1"
auto_editor/__main__.py CHANGED
@@ -8,16 +8,15 @@ from subprocess import run
8
8
 
9
9
  import auto_editor
10
10
  from auto_editor.edit import edit_media
11
- from auto_editor.ffwrapper import FFmpeg, initFFmpeg
11
+ from auto_editor.ffwrapper import FFmpeg
12
12
  from auto_editor.utils.func import get_stdout
13
13
  from auto_editor.utils.log import Log
14
14
  from auto_editor.utils.types import (
15
15
  Args,
16
- bitrate,
17
- color,
18
16
  frame_rate,
19
17
  margin,
20
18
  number,
19
+ parse_color,
21
20
  resolution,
22
21
  sample_rate,
23
22
  speed,
@@ -109,7 +108,7 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
109
108
  parser.add_argument(
110
109
  "--background",
111
110
  "-b",
112
- type=color,
111
+ type=parse_color,
113
112
  metavar="COLOR",
114
113
  help="Set the background as a solid RGB color",
115
114
  )
@@ -167,11 +166,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
167
166
  metavar="PATH",
168
167
  help="Set a custom path to the ffmpeg location",
169
168
  )
170
- parser.add_argument(
171
- "--my-ffmpeg",
172
- flag=True,
173
- help="Use the ffmpeg on your PATH instead of the one packaged",
174
- )
175
169
  parser.add_text("Display Options:")
176
170
  parser.add_argument(
177
171
  "--progress",
@@ -180,12 +174,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
180
174
  help="Set what type of progress bar to use",
181
175
  )
182
176
  parser.add_argument("--debug", flag=True, help="Show debugging messages and values")
183
- parser.add_argument(
184
- "--show-ffmpeg-commands", flag=True, help="Show ffmpeg commands"
185
- )
186
- parser.add_argument(
187
- "--show-ffmpeg-output", flag=True, help="Show ffmpeg stdout and stderr"
188
- )
189
177
  parser.add_argument("--quiet", "-q", flag=True, help="Display less output")
190
178
  parser.add_argument(
191
179
  "--preview",
@@ -205,16 +193,8 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
205
193
  "--video-bitrate",
206
194
  "-b:v",
207
195
  metavar="BITRATE",
208
- type=bitrate,
209
196
  help="Set the number of bits per second for video",
210
197
  )
211
- parser.add_argument(
212
- "--video-quality-scale",
213
- "-qscale:v",
214
- "-q:v",
215
- metavar="SCALE",
216
- help="Set a value to the ffmpeg option -qscale:v",
217
- )
218
198
  parser.add_argument(
219
199
  "--scale",
220
200
  type=number,
@@ -238,7 +218,6 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
238
218
  "--audio-bitrate",
239
219
  "-b:a",
240
220
  metavar="BITRATE",
241
- type=bitrate,
242
221
  help="Set the number of bits per second for audio",
243
222
  )
244
223
  parser.add_argument(
@@ -281,7 +260,7 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
281
260
  log.conwrite("Downloading video...")
282
261
 
283
262
  def get_domain(url: str) -> str:
284
- t = __import__("urllib").parse.urlparse(url).netloc
263
+ t = __import__("urllib.parse", fromlist=["parse"]).urlparse(url).netloc
285
264
  return ".".join(t.split(".")[-2:])
286
265
 
287
266
  download_format = args.download_format
@@ -295,7 +274,7 @@ def download_video(my_input: str, args: Args, ffmpeg: FFmpeg, log: Log) -> str:
295
274
 
296
275
  yt_dlp_path = args.yt_dlp_location
297
276
 
298
- cmd = ["--ffmpeg-location", ffmpeg.path]
277
+ cmd = ["--ffmpeg-location", ffmpeg.get_path("yt-dlp", log)]
299
278
 
300
279
  if download_format is not None:
301
280
  cmd.extend(["-f", download_format])
@@ -373,13 +352,7 @@ def main() -> None:
373
352
  is_machine = args.progress == "machine"
374
353
  log = Log(args.debug, args.quiet, args.temp_dir, is_machine, no_color)
375
354
 
376
- ffmpeg = initFFmpeg(
377
- log,
378
- args.ffmpeg_location,
379
- args.my_ffmpeg,
380
- args.show_ffmpeg_commands,
381
- args.show_ffmpeg_output,
382
- )
355
+ ffmpeg = FFmpeg(args.ffmpeg_location)
383
356
  paths = []
384
357
  for my_input in args.input:
385
358
  if my_input.startswith("http://") or my_input.startswith("https://"):
auto_editor/edit.py CHANGED
@@ -2,13 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import sys
5
+ from fractions import Fraction
6
+ from os.path import splitext
5
7
  from subprocess import run
6
8
  from typing import Any
7
9
 
10
+ import av
11
+ from av import AudioResampler
12
+
8
13
  from auto_editor.ffwrapper import FFmpeg, FileInfo, initFileInfo
9
14
  from auto_editor.lib.contracts import is_int, is_str
10
- from auto_editor.make_layers import make_timeline
11
- from auto_editor.output import Ensure, mux_quality_media
15
+ from auto_editor.make_layers import clipify, make_av, make_timeline
16
+ from auto_editor.output import Ensure, parse_bitrate
12
17
  from auto_editor.render.audio import make_new_audio
13
18
  from auto_editor.render.subtitle import make_new_subtitles
14
19
  from auto_editor.render.video import render_av
@@ -27,7 +32,7 @@ def set_output(
27
32
  if src is None:
28
33
  root, ext = "out", ".mp4"
29
34
  else:
30
- root, ext = os.path.splitext(str(src.path) if out is None else out)
35
+ root, ext = splitext(src.path if out is None else out)
31
36
  if ext == "":
32
37
  ext = src.path.suffix
33
38
 
@@ -92,11 +97,19 @@ def set_audio_codec(
92
97
  codec: str, src: FileInfo | None, out_ext: str, ctr: Container, log: Log
93
98
  ) -> str:
94
99
  if codec == "auto":
95
- codec = "aac" if (src is None or not src.audios) else src.audios[0].codec
100
+ if src is None or not src.audios:
101
+ codec = "aac"
102
+ else:
103
+ codec = src.audios[0].codec
104
+ ctx = av.Codec(codec)
105
+ if ctx.audio_formats is None:
106
+ codec = "aac"
96
107
  if codec not in ctr.acodecs and ctr.default_aud != "none":
97
- return ctr.default_aud
108
+ codec = ctr.default_aud
98
109
  if codec == "mp3float":
99
- return "mp3"
110
+ codec = "mp3"
111
+ if codec is None:
112
+ codec = "aac"
100
113
  return codec
101
114
 
102
115
  if codec == "copy":
@@ -106,9 +119,8 @@ def set_audio_codec(
106
119
  log.error("Input file does not have an audio stream to copy codec from.")
107
120
  codec = src.audios[0].codec
108
121
 
109
- if codec != "unset":
110
- if ctr.acodecs is None or codec not in ctr.acodecs:
111
- log.error(codec_error.format(codec, out_ext))
122
+ if ctr.acodecs is None or codec not in ctr.acodecs:
123
+ log.error(codec_error.format(codec, out_ext))
112
124
 
113
125
  return codec
114
126
 
@@ -153,7 +165,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
153
165
  tl = None
154
166
 
155
167
  if paths:
156
- path_ext = os.path.splitext(paths[0])[1].lower()
168
+ path_ext = splitext(paths[0])[1].lower()
157
169
  if path_ext == ".xml":
158
170
  from auto_editor.formats.fcp7 import fcp7_read_xml
159
171
 
@@ -232,7 +244,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
232
244
  from auto_editor.formats.fcp7 import fcp7_write_xml
233
245
 
234
246
  is_resolve = export.startswith("resolve")
235
- fcp7_write_xml(export_ops["name"], output, is_resolve, tl, log)
247
+ fcp7_write_xml(export_ops["name"], output, is_resolve, tl)
236
248
  return
237
249
 
238
250
  if export == "final-cut-pro":
@@ -256,7 +268,7 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
256
268
  shotcut_write_mlt(output, tl)
257
269
  return
258
270
 
259
- out_ext = os.path.splitext(output)[1].replace(".", "")
271
+ out_ext = splitext(output)[1].replace(".", "")
260
272
 
261
273
  # Check if export options make sense.
262
274
  ctr = container_constructor(out_ext.lower())
@@ -270,62 +282,144 @@ def edit_media(paths: list[str], ffmpeg: FFmpeg, args: Args, log: Log) -> None:
270
282
  if args.keep_tracks_separate and ctr.max_audios == 1:
271
283
  log.warning(f"'{out_ext}' container doesn't support multiple audio tracks.")
272
284
 
273
- def make_media(tl: v3, output: str) -> None:
285
+ def make_media(tl: v3, output_path: str) -> None:
274
286
  assert src is not None
275
287
 
276
- visual_output = []
277
- audio_output = []
278
- sub_output = []
279
- apply_later = False
288
+ output = av.open(output_path, "w")
280
289
 
281
- ensure = Ensure(ffmpeg, bar, samplerate, log)
282
290
  if ctr.default_sub != "none" and not args.sn:
283
- sub_output = make_new_subtitles(tl, ensure, log.temp)
291
+ sub_paths = make_new_subtitles(tl, log)
292
+ else:
293
+ sub_paths = []
284
294
 
285
295
  if ctr.default_aud != "none":
286
- audio_output = make_new_audio(tl, ensure, args, ffmpeg, bar, log)
287
-
288
- if ctr.default_vid != "none":
289
- if tl.v:
290
- out_path, apply_later = render_av(ffmpeg, tl, args, bar, ctr, log)
291
- visual_output.append((True, out_path))
292
-
293
- for v, vid in enumerate(src.videos, start=1):
294
- if ctr.allow_image and vid.codec in ("png", "mjpeg", "webp"):
295
- out_path = os.path.join(log.temp, f"{v}.{vid.codec}")
296
- # fmt: off
297
- ffmpeg.run(["-i", f"{src.path}", "-map", "0:v", "-map", "-0:V",
298
- "-c", "copy", out_path])
299
- # fmt: on
300
- visual_output.append((False, out_path))
301
-
302
- log.conwrite("Writing output file")
303
- mux_quality_media(
304
- ffmpeg,
305
- visual_output,
306
- audio_output,
307
- sub_output,
308
- apply_later,
309
- ctr,
310
- output,
311
- tl.tb,
312
- args,
313
- src,
314
- log,
315
- )
296
+ ensure = Ensure(bar, samplerate, log)
297
+ audio_paths = make_new_audio(tl, ctr, ensure, args, ffmpeg, bar, log)
298
+ else:
299
+ audio_paths = []
300
+
301
+ # Setup audio
302
+ if audio_paths:
303
+ try:
304
+ audio_encoder = av.Codec(args.audio_codec)
305
+ except av.FFmpegError as e:
306
+ log.error(e)
307
+ if audio_encoder.audio_formats is None:
308
+ log.error(f"{args.audio_codec}: No known audio formats avail.")
309
+ audio_format = audio_encoder.audio_formats[0]
310
+ resampler = AudioResampler(format=audio_format, layout="stereo", rate=tl.sr)
311
+
312
+ audio_streams: list[av.AudioStream] = []
313
+ audio_inputs = []
314
+ audio_gen_frames = []
315
+ for i, audio_path in enumerate(audio_paths):
316
+ audio_stream = output.add_stream(
317
+ args.audio_codec,
318
+ format=audio_format,
319
+ rate=tl.sr,
320
+ time_base=Fraction(1, tl.sr),
321
+ )
322
+ if not isinstance(audio_stream, av.AudioStream):
323
+ log.error(f"Not a known audio codec: {args.audio_codec}")
324
+
325
+ if args.audio_bitrate != "auto":
326
+ audio_stream.bit_rate = parse_bitrate(args.audio_bitrate, log)
327
+ log.debug(f"audio bitrate: {audio_stream.bit_rate}")
328
+ else:
329
+ log.debug(f"[auto] audio bitrate: {audio_stream.bit_rate}")
330
+ if i < len(src.audios) and src.audios[i].lang is not None:
331
+ audio_stream.metadata["language"] = src.audios[i].lang # type: ignore
332
+
333
+ audio_streams.append(audio_stream)
334
+ audio_input = av.open(audio_path)
335
+ audio_inputs.append(audio_input)
336
+ audio_gen_frames.append(audio_input.decode(audio=0))
337
+
338
+ # Setup subtitles
339
+ subtitle_streams = []
340
+ subtitle_inputs = []
341
+ sub_gen_frames = []
342
+
343
+ for i, sub_path in enumerate(sub_paths):
344
+ subtitle_input = av.open(sub_path)
345
+ subtitle_inputs.append(subtitle_input)
346
+ subtitle_stream = output.add_stream(
347
+ template=subtitle_input.streams.subtitles[0]
348
+ )
349
+ if i < len(src.subtitles) and src.subtitles[i].lang is not None:
350
+ subtitle_stream.metadata["language"] = src.subtitles[i].lang # type: ignore
351
+
352
+ subtitle_streams.append(subtitle_stream)
353
+ sub_gen_frames.append(subtitle_input.demux(subtitles=0))
354
+
355
+ # Setup video
356
+ if ctr.default_vid != "none" and tl.v:
357
+ vframes = render_av(output, tl, args, bar, log)
358
+ output_stream = next(vframes)
359
+ else:
360
+ output_stream, vframes = None, iter([])
361
+
362
+ # Process frames
363
+ while True:
364
+ audio_frames = [next(frames, None) for frames in audio_gen_frames]
365
+ video_frame = next(vframes, None)
366
+ subtitle_frames = [next(packet, None) for packet in sub_gen_frames]
367
+
368
+ if (
369
+ all(frame is None for frame in audio_frames)
370
+ and video_frame is None
371
+ and all(packet is None for packet in subtitle_frames)
372
+ ):
373
+ break
374
+
375
+ for audio_stream, audio_frame in zip(audio_streams, audio_frames):
376
+ if audio_frame:
377
+ for reframe in resampler.resample(audio_frame):
378
+ output.mux(audio_stream.encode(reframe))
379
+
380
+ for subtitle_stream, packet in zip(subtitle_streams, subtitle_frames):
381
+ if not packet or packet.dts is None:
382
+ continue
383
+ packet.stream = subtitle_stream
384
+ output.mux(packet)
385
+
386
+ if video_frame:
387
+ try:
388
+ output.mux(output_stream.encode(video_frame))
389
+ except av.error.ExternalError:
390
+ log.error(
391
+ f"Generic error for encoder: {output_stream.name}\n"
392
+ "Perhaps video quality settings are too low?"
393
+ )
394
+ except av.FFmpegError as e:
395
+ log.error(e)
396
+
397
+ # Flush streams
398
+ if output_stream is not None:
399
+ output.mux(output_stream.encode(None))
400
+ for audio_stream in audio_streams:
401
+ output.mux(audio_stream.encode(None))
402
+
403
+ # Close resources
404
+ for audio_input in audio_inputs:
405
+ audio_input.close()
406
+ for subtitle_input in subtitle_inputs:
407
+ subtitle_input.close()
408
+ output.close()
316
409
 
317
410
  if export == "clip-sequence":
318
411
  if tl.v1 is None:
319
412
  log.error("Timeline too complex to use clip-sequence export")
320
413
 
321
- from auto_editor.make_layers import clipify, make_av
322
- from auto_editor.utils.func import append_filename
323
-
324
414
  def pad_chunk(chunk: Chunk, total: int) -> Chunks:
325
415
  start = [] if chunk[0] == 0 else [(0, chunk[0], 99999.0)]
326
416
  end = [] if chunk[1] == total else [(chunk[1], total, 99999.0)]
327
417
  return start + [chunk] + end
328
418
 
419
+ def append_filename(path: str, val: str) -> str:
420
+ root, ext = splitext(path)
421
+ return root + val + ext
422
+
329
423
  total_frames = tl.v1.chunks[-1][1] - 1
330
424
  clip_num = 0
331
425
  for chunk in tl.v1.chunks:
auto_editor/ffwrapper.py CHANGED
@@ -1,98 +1,40 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os.path
4
- import sys
5
3
  from dataclasses import dataclass
6
4
  from fractions import Fraction
7
5
  from pathlib import Path
8
- from re import search
9
6
  from shutil import which
10
- from subprocess import PIPE, Popen, run
11
- from typing import Any
7
+ from subprocess import PIPE, Popen
12
8
 
13
9
  import av
14
10
 
15
11
  from auto_editor.utils.log import Log
16
12
 
17
13
 
18
- def initFFmpeg(
19
- log: Log, ff_location: str | None, my_ffmpeg: bool, show_cmd: bool, debug: bool
20
- ) -> FFmpeg:
21
- if ff_location is not None:
22
- program = ff_location
23
- elif my_ffmpeg:
24
- program = "ffmpeg"
25
- else:
26
- try:
27
- import ae_ffmpeg
28
-
29
- program = ae_ffmpeg.get_path()
30
- except ImportError:
31
- program = "ffmpeg"
32
-
33
- path: str | None = which(program)
34
- if path is None:
35
- log.error("Did not find ffmpeg on PATH.")
36
-
37
- return FFmpeg(log, path, show_cmd, debug)
14
+ def _get_ffmpeg(reason: str, ffloc: str | None, log: Log) -> str:
15
+ program = "ffmpeg" if ffloc is None else ffloc
16
+ if (path := which(program)) is None:
17
+ log.error(f"{reason} needs ffmpeg cli but couldn't find ffmpeg on PATH.")
18
+ return path
38
19
 
39
20
 
40
21
  @dataclass(slots=True)
41
22
  class FFmpeg:
42
- log: Log
43
- path: str
44
- show_cmd: bool
45
- debug: bool
46
-
47
- def run(self, cmd: list[str]) -> None:
48
- cmd = [self.path, "-hide_banner", "-y"] + cmd
49
- if not self.debug:
50
- cmd.extend(["-nostats", "-loglevel", "error"])
51
- if self.show_cmd:
52
- sys.stderr.write(f"{' '.join(cmd)}\n\n")
53
- run(cmd)
54
-
55
- def run_check_errors(
56
- self, cmd: list[str], show_out: bool = False, path: str | None = None
57
- ) -> None:
58
- process = self.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
59
- _, stderr = process.communicate()
60
-
61
- if process.stdin is not None:
62
- process.stdin.close()
63
- output = stderr.decode("utf-8", "replace")
64
-
65
- error_list = (
66
- r"Unknown encoder '.*'",
67
- r"-q:v qscale not available for encoder\. Use -b:v bitrate instead\.",
68
- r"Specified sample rate .* is not supported",
69
- r'Unable to parse option value ".*"',
70
- r"Error setting option .* to value .*\.",
71
- r"Undefined constant or missing '.*' in '.*'",
72
- r"DLL .* failed to open",
73
- r"Incompatible pixel format '.*' for codec '[A-Za-z0-9_]*'",
74
- r"Unrecognized option '.*'",
75
- r"Permission denied",
76
- )
23
+ ffmpeg_location: str | None
24
+ path: str | None = None
77
25
 
78
- if self.debug:
79
- print(f"stderr: {output}")
26
+ def get_path(self, reason: str, log: Log) -> str:
27
+ if self.path is not None:
28
+ return self.path
80
29
 
81
- for item in error_list:
82
- if check := search(item, output):
83
- self.log.error(check.group())
30
+ self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
31
+ return self.path
84
32
 
85
- if path is not None and not os.path.isfile(path):
86
- self.log.error(f"The file {path} was not created.")
87
- if show_out and not self.debug:
88
- print(f"stderr: {output}")
33
+ def Popen(self, reason: str, cmd: list[str], log: Log) -> Popen:
34
+ if self.path is None:
35
+ self.path = _get_ffmpeg(reason, self.ffmpeg_location, log)
89
36
 
90
- def Popen(
91
- self, cmd: list[str], stdin: Any = None, stdout: Any = PIPE, stderr: Any = None
92
- ) -> Popen:
93
- if self.show_cmd:
94
- sys.stderr.write(f"{self.path} {' '.join(cmd)}\n\n")
95
- return Popen([self.path] + cmd, stdin=stdin, stdout=stdout, stderr=stderr)
37
+ return Popen([self.path] + cmd, stdout=PIPE, stderr=PIPE)
96
38
 
97
39
 
98
40
  def mux(input: Path, output: Path, stream: int) -> None:
@@ -103,13 +45,9 @@ def mux(input: Path, output: Path, stream: int) -> None:
103
45
  output_audio_stream = output_container.add_stream("pcm_s16le")
104
46
 
105
47
  for frame in input_container.decode(input_audio_stream):
106
- packet = output_audio_stream.encode(frame)
107
- if packet:
108
- output_container.mux(packet)
48
+ output_container.mux(output_audio_stream.encode(frame))
109
49
 
110
- packet = output_audio_stream.encode(None)
111
- if packet:
112
- output_container.mux(packet)
50
+ output_container.mux(output_audio_stream.encode(None))
113
51
 
114
52
  output_container.close()
115
53
  input_container.close()
@@ -485,7 +485,7 @@ def premiere_write_audio(audio: Element, make_filedef, src: FileInfo, tl: v3) ->
485
485
  audio.append(track)
486
486
 
487
487
 
488
- def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3, log: Log) -> None:
488
+ def fcp7_write_xml(name: str, output: str, resolve: bool, tl: v3) -> None:
489
489
  width, height = tl.res
490
490
  timebase, ntsc = set_tb_ntsc(tl.tb)
491
491
 
auto_editor/help.py CHANGED
@@ -148,11 +148,12 @@ Beware that the temp directory can get quite big.
148
148
  "--my-ffmpeg": "This is equivalent to `--ffmpeg-location ffmpeg`.",
149
149
  "--audio-bitrate": """
150
150
  `--audio-bitrate` sets the target bitrate for the audio encoder.
151
- The value accepts a natural number and the units: ``, `k`, `K`, and `M`.
152
- The special value `unset` may also be used, and means: Don't pass any value to ffmpeg, let it choose a default bitrate.
151
+ By default, the value is `auto` (let the encoder decide).
152
+ It can be set to a natural number with units: ``, `k`, `K`, `M`, or `G`.
153
+
153
154
  """.strip(),
154
155
  "--video-bitrate": """
155
- `--video-bitrate` sets the target bitrate for the video encoder. It accepts the same format as `--audio-bitrate` and the special `unset` value is allowed.
156
+ `--video-bitrate` sets the target bitrate for the video encoder. `auto` is set as the default. It accepts the same format as `--audio-bitrate`
156
157
  """.strip(),
157
158
  "--margin": """
158
159
  Default value: 0.2s,0.2s
auto_editor/lang/palet.py CHANGED
@@ -353,9 +353,7 @@ class Lexer:
353
353
  if is_method:
354
354
  from auto_editor.utils.cmdkw import parse_method
355
355
 
356
- return Token(
357
- M, parse_method(name, result, env), self.lineno, self.column
358
- )
356
+ return Token(M, parse_method(name, result), self.lineno, self.column)
359
357
 
360
358
  if self.char == ".": # handle `object.method` syntax
361
359
  self.advance()
@@ -635,6 +633,8 @@ def edit_subtitle(pattern, stream=0, **kwargs):
635
633
 
636
634
 
637
635
  class StackTraceManager:
636
+ __slots__ = ("stack",)
637
+
638
638
  def __init__(self) -> None:
639
639
  self.stack: list[Sym] = []
640
640
 
@@ -645,12 +645,6 @@ class StackTraceManager:
645
645
  if self.stack:
646
646
  self.stack.pop()
647
647
 
648
- def get_stacktrace(self) -> str:
649
- return "\n".join(
650
- f" at {sym.val} ({sym.lineno}:{sym.column})"
651
- for sym in reversed(self.stack)
652
- )
653
-
654
648
 
655
649
  stack_trace_manager = StackTraceManager()
656
650
 
@@ -14,7 +14,6 @@ if TYPE_CHECKING:
14
14
  from numpy.typing import NDArray
15
15
 
16
16
  Number = int | float | complex | Fraction
17
- Real = int | float | Fraction
18
17
  BoolList = NDArray[np.bool_]
19
18
  Node = tuple
20
19
 
@@ -831,12 +830,6 @@ def make_standard_env() -> dict[str, Any]:
831
830
  check_args("xor", vals, (2, None), (is_bool,))
832
831
  return reduce(lambda a, b: a ^ b, vals)
833
832
 
834
- def string_ref(s: str, ref: int) -> Char:
835
- try:
836
- return Char(s[ref])
837
- except IndexError:
838
- raise MyError(f"string index {ref} is out of range")
839
-
840
833
  def number_to_string(val: Number) -> str:
841
834
  if isinstance(val, complex):
842
835
  join = "" if val.imag < 0 else "+"