auto-editor 26.3.3__py3-none-any.whl → 27.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_editor/__init__.py +1 -1
- auto_editor/__main__.py +9 -5
- auto_editor/analyze.py +30 -36
- auto_editor/cmds/info.py +1 -1
- auto_editor/cmds/levels.py +3 -3
- auto_editor/cmds/subdump.py +62 -8
- auto_editor/cmds/test.py +56 -40
- auto_editor/edit.py +34 -44
- auto_editor/ffwrapper.py +9 -9
- auto_editor/formats/json.py +2 -2
- auto_editor/{lang/json.py → json.py} +39 -43
- auto_editor/lang/palet.py +2 -2
- auto_editor/lang/stdenv.py +12 -0
- auto_editor/output.py +4 -4
- auto_editor/render/audio.py +26 -24
- auto_editor/render/subtitle.py +10 -14
- auto_editor/render/video.py +40 -44
- auto_editor/utils/container.py +3 -3
- {auto_editor-26.3.3.dist-info → auto_editor-27.0.0.dist-info}/METADATA +7 -6
- {auto_editor-26.3.3.dist-info → auto_editor-27.0.0.dist-info}/RECORD +25 -25
- {auto_editor-26.3.3.dist-info → auto_editor-27.0.0.dist-info}/WHEEL +1 -1
- docs/build.py +16 -7
- {auto_editor-26.3.3.dist-info → auto_editor-27.0.0.dist-info}/entry_points.txt +0 -0
- {auto_editor-26.3.3.dist-info → auto_editor-27.0.0.dist-info/licenses}/LICENSE +0 -0
- {auto_editor-26.3.3.dist-info → auto_editor-27.0.0.dist-info}/top_level.txt +0 -0
auto_editor/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "
|
1
|
+
__version__ = "27.0.0"
|
auto_editor/__main__.py
CHANGED
@@ -76,6 +76,7 @@ class Args:
|
|
76
76
|
# Audio Rendering
|
77
77
|
audio_codec: str = "auto"
|
78
78
|
audio_bitrate: str = "auto"
|
79
|
+
mix_audio_streams: bool = False
|
79
80
|
keep_tracks_separate: bool = False
|
80
81
|
audio_normalize: str = "#f"
|
81
82
|
|
@@ -341,10 +342,13 @@ def main_options(parser: ArgumentParser) -> ArgumentParser:
|
|
341
342
|
metavar="BITRATE",
|
342
343
|
help="Set the number of bits per second for audio",
|
343
344
|
)
|
345
|
+
parser.add_argument(
|
346
|
+
"--mix-audio-streams", flag=True, help="Mix all audio streams together into one"
|
347
|
+
)
|
344
348
|
parser.add_argument(
|
345
349
|
"--keep-tracks-separate",
|
346
350
|
flag=True,
|
347
|
-
help="Don't mix all audio
|
351
|
+
help="Don't mix all audio streams into one when exporting (default)",
|
348
352
|
)
|
349
353
|
parser.add_argument(
|
350
354
|
"--audio-normalize",
|
@@ -448,15 +452,15 @@ def main() -> None:
|
|
448
452
|
if args.debug and not args.input:
|
449
453
|
buf = StringIO()
|
450
454
|
buf.write(f"OS: {plat.system()} {plat.release()} {plat.machine().lower()}\n")
|
451
|
-
buf.write(f"Python: {plat.python_version()}\
|
455
|
+
buf.write(f"Python: {plat.python_version()}\nAV: ")
|
452
456
|
try:
|
453
|
-
import
|
457
|
+
import bv
|
454
458
|
except (ModuleNotFoundError, ImportError):
|
455
459
|
buf.write("not found")
|
456
460
|
else:
|
457
461
|
try:
|
458
|
-
buf.write(f"{
|
459
|
-
license =
|
462
|
+
buf.write(f"{bv.__version__} ")
|
463
|
+
license = bv._core.library_meta["libavcodec"]["license"]
|
460
464
|
buf.write(f"({license})")
|
461
465
|
except AttributeError:
|
462
466
|
buf.write("error")
|
auto_editor/analyze.py
CHANGED
@@ -9,10 +9,10 @@ from math import ceil
|
|
9
9
|
from tempfile import gettempdir
|
10
10
|
from typing import TYPE_CHECKING
|
11
11
|
|
12
|
-
import
|
12
|
+
import bv
|
13
13
|
import numpy as np
|
14
|
-
from
|
15
|
-
from
|
14
|
+
from bv.audio.fifo import AudioFifo
|
15
|
+
from bv.subtitles.subtitle import AssSubtitle
|
16
16
|
|
17
17
|
from auto_editor import __version__
|
18
18
|
|
@@ -72,7 +72,7 @@ def mut_remove_large(
|
|
72
72
|
active = False
|
73
73
|
|
74
74
|
|
75
|
-
def iter_audio(audio_stream:
|
75
|
+
def iter_audio(audio_stream: bv.AudioStream, tb: Fraction) -> Iterator[np.float32]:
|
76
76
|
fifo = AudioFifo()
|
77
77
|
sr = audio_stream.rate
|
78
78
|
|
@@ -80,10 +80,10 @@ def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float3
|
|
80
80
|
accumulated_error = Fraction(0)
|
81
81
|
|
82
82
|
# Resample so that audio data is between [-1, 1]
|
83
|
-
resampler =
|
83
|
+
resampler = bv.AudioResampler(bv.AudioFormat("flt"), audio_stream.layout, sr)
|
84
84
|
|
85
85
|
container = audio_stream.container
|
86
|
-
assert isinstance(container,
|
86
|
+
assert isinstance(container, bv.container.InputContainer)
|
87
87
|
|
88
88
|
for frame in container.decode(audio_stream):
|
89
89
|
frame.pts = None # Skip time checks
|
@@ -103,7 +103,7 @@ def iter_audio(audio_stream: av.AudioStream, tb: Fraction) -> Iterator[np.float3
|
|
103
103
|
|
104
104
|
|
105
105
|
def iter_motion(
|
106
|
-
video:
|
106
|
+
video: bv.VideoStream, tb: Fraction, blur: int, width: int
|
107
107
|
) -> Iterator[np.float32]:
|
108
108
|
video.thread_type = "AUTO"
|
109
109
|
|
@@ -113,7 +113,7 @@ def iter_motion(
|
|
113
113
|
index = 0
|
114
114
|
prev_index = -1
|
115
115
|
|
116
|
-
graph =
|
116
|
+
graph = bv.filter.Graph()
|
117
117
|
graph.link_nodes(
|
118
118
|
graph.add_buffer(template=video),
|
119
119
|
graph.add("scale", f"{width}:-1"),
|
@@ -123,7 +123,7 @@ def iter_motion(
|
|
123
123
|
).configure()
|
124
124
|
|
125
125
|
container = video.container
|
126
|
-
assert isinstance(container,
|
126
|
+
assert isinstance(container, bv.container.InputContainer)
|
127
127
|
|
128
128
|
for unframe in container.decode(video):
|
129
129
|
if unframe.pts is None:
|
@@ -154,7 +154,7 @@ def iter_motion(
|
|
154
154
|
|
155
155
|
@dataclass(slots=True)
|
156
156
|
class Levels:
|
157
|
-
container:
|
157
|
+
container: bv.container.InputContainer
|
158
158
|
name: str
|
159
159
|
mod_time: int
|
160
160
|
tb: Fraction
|
@@ -258,7 +258,7 @@ class Levels:
|
|
258
258
|
if audio.duration is not None and audio.time_base is not None:
|
259
259
|
inaccurate_dur = int(audio.duration * audio.time_base * self.tb)
|
260
260
|
elif container.duration is not None:
|
261
|
-
inaccurate_dur = int(container.duration /
|
261
|
+
inaccurate_dur = int(container.duration / bv.time_base * self.tb)
|
262
262
|
else:
|
263
263
|
inaccurate_dur = 1024
|
264
264
|
|
@@ -343,15 +343,9 @@ class Levels:
|
|
343
343
|
for packet in container.demux(subtitle_stream):
|
344
344
|
if packet.pts is None or packet.duration is None:
|
345
345
|
continue
|
346
|
-
|
347
|
-
# See definition of `AVSubtitle`
|
348
|
-
# in: https://ffmpeg.org/doxygen/trunk/avcodec_8h_source.html
|
349
|
-
start = float(packet.pts * subtitle_stream.time_base)
|
350
|
-
dur = float(packet.duration * subtitle_stream.time_base)
|
351
|
-
|
352
|
-
end = round((start + dur) * self.tb)
|
353
|
-
sub_length = max(sub_length, end)
|
346
|
+
sub_length = max(sub_length, packet.pts + packet.duration)
|
354
347
|
|
348
|
+
sub_length = round(sub_length * subtitle_stream.time_base * self.tb)
|
355
349
|
result = np.zeros((sub_length), dtype=np.bool_)
|
356
350
|
del sub_length
|
357
351
|
|
@@ -363,25 +357,25 @@ class Levels:
|
|
363
357
|
continue
|
364
358
|
if early_exit:
|
365
359
|
break
|
366
|
-
for subset in packet.decode():
|
367
|
-
if max_count is not None and count >= max_count:
|
368
|
-
early_exit = True
|
369
|
-
break
|
370
360
|
|
371
|
-
|
372
|
-
|
361
|
+
if max_count is not None and count >= max_count:
|
362
|
+
early_exit = True
|
363
|
+
break
|
364
|
+
|
365
|
+
start = float(packet.pts * subtitle_stream.time_base)
|
366
|
+
dur = float(packet.duration * subtitle_stream.time_base)
|
373
367
|
|
374
|
-
|
375
|
-
|
368
|
+
san_start = round(start * self.tb)
|
369
|
+
san_end = round((start + dur) * self.tb)
|
376
370
|
|
377
|
-
|
378
|
-
|
379
|
-
|
371
|
+
for sub in packet.decode():
|
372
|
+
if not isinstance(sub, AssSubtitle):
|
373
|
+
continue
|
380
374
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
375
|
+
line = sub.dialogue.decode(errors="ignore")
|
376
|
+
if line and re.search(re_pattern, line):
|
377
|
+
result[san_start:san_end] = 1
|
378
|
+
count += 1
|
385
379
|
|
386
380
|
container.seek(0)
|
387
381
|
return result
|
@@ -391,8 +385,8 @@ def initLevels(
|
|
391
385
|
src: FileInfo, tb: Fraction, bar: Bar, no_cache: bool, log: Log
|
392
386
|
) -> Levels:
|
393
387
|
try:
|
394
|
-
container =
|
395
|
-
except
|
388
|
+
container = bv.open(src.path)
|
389
|
+
except bv.FFmpegError as e:
|
396
390
|
log.error(e)
|
397
391
|
|
398
392
|
mod_time = int(src.path.stat().st_mtime)
|
auto_editor/cmds/info.py
CHANGED
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
|
|
6
6
|
from typing import Any, Literal, TypedDict
|
7
7
|
|
8
8
|
from auto_editor.ffwrapper import initFileInfo
|
9
|
-
from auto_editor.
|
9
|
+
from auto_editor.json import dump
|
10
10
|
from auto_editor.make_layers import make_sane_timebase
|
11
11
|
from auto_editor.timeline import v3
|
12
12
|
from auto_editor.utils.func import aspect_ratio
|
auto_editor/cmds/levels.py
CHANGED
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|
5
5
|
from fractions import Fraction
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
-
import
|
8
|
+
import bv
|
9
9
|
import numpy as np
|
10
10
|
|
11
11
|
from auto_editor.analyze import *
|
@@ -134,7 +134,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
134
134
|
if (arr := levels.read_cache("audio", (obj["stream"],))) is not None:
|
135
135
|
print_arr(arr)
|
136
136
|
else:
|
137
|
-
container =
|
137
|
+
container = bv.open(src.path, "r")
|
138
138
|
audio_stream = container.streams.audio[obj["stream"]]
|
139
139
|
|
140
140
|
values = []
|
@@ -155,7 +155,7 @@ def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
|
155
155
|
if (arr := levels.read_cache("motion", mobj)) is not None:
|
156
156
|
print_arr(arr)
|
157
157
|
else:
|
158
|
-
container =
|
158
|
+
container = bv.open(src.path, "r")
|
159
159
|
video_stream = container.streams.video[obj["stream"]]
|
160
160
|
|
161
161
|
values = []
|
auto_editor/cmds/subdump.py
CHANGED
@@ -1,18 +1,72 @@
|
|
1
1
|
import sys
|
2
|
+
from dataclasses import dataclass, field
|
2
3
|
|
3
|
-
import
|
4
|
-
from
|
4
|
+
import bv
|
5
|
+
from bv.subtitles.subtitle import AssSubtitle
|
6
|
+
|
7
|
+
from auto_editor.json import dump
|
8
|
+
from auto_editor.vanparse import ArgumentParser
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass(slots=True)
|
12
|
+
class SubdumpArgs:
|
13
|
+
help: bool = False
|
14
|
+
input: list[str] = field(default_factory=list)
|
15
|
+
json: bool = False
|
5
16
|
|
6
17
|
|
7
18
|
def main(sys_args: list[str] = sys.argv[1:]) -> None:
|
8
|
-
|
9
|
-
|
19
|
+
parser = ArgumentParser("subdump")
|
20
|
+
parser.add_required("input", nargs="*")
|
21
|
+
parser.add_argument("--json", flag=True)
|
22
|
+
args = parser.parse_args(SubdumpArgs, sys_args)
|
23
|
+
|
24
|
+
do_filter = True
|
25
|
+
|
26
|
+
if args.json:
|
27
|
+
data = {}
|
28
|
+
for input_file in args.input:
|
29
|
+
container = bv.open(input_file)
|
10
30
|
for s in range(len(container.streams.subtitles)):
|
11
|
-
|
12
|
-
|
13
|
-
|
31
|
+
entry_data = []
|
32
|
+
|
33
|
+
input_stream = container.streams.subtitles[s]
|
34
|
+
assert input_stream.time_base is not None
|
35
|
+
for packet in container.demux(input_stream):
|
36
|
+
if (
|
37
|
+
packet.dts is None
|
38
|
+
or packet.pts is None
|
39
|
+
or packet.duration is None
|
40
|
+
):
|
41
|
+
continue
|
42
|
+
|
43
|
+
start = packet.pts * input_stream.time_base
|
44
|
+
end = start + packet.duration * input_stream.time_base
|
45
|
+
|
46
|
+
startf = round(float(start), 3)
|
47
|
+
endf = round(float(end), 3)
|
48
|
+
|
49
|
+
if do_filter and endf - startf <= 0.02:
|
50
|
+
continue
|
51
|
+
|
52
|
+
for sub in packet.decode():
|
14
53
|
if isinstance(sub, AssSubtitle):
|
15
|
-
|
54
|
+
content = sub.dialogue.decode("utf-8", errors="ignore")
|
55
|
+
entry_data.append([startf, endf, content])
|
56
|
+
|
57
|
+
data[f"{input_file}:{s}"] = entry_data
|
58
|
+
container.close()
|
59
|
+
|
60
|
+
dump(data, sys.stdout, indent=4)
|
61
|
+
return
|
62
|
+
|
63
|
+
for i, input_file in enumerate(args.input):
|
64
|
+
with bv.open(input_file) as container:
|
65
|
+
for s in range(len(container.streams.subtitles)):
|
66
|
+
print(f"file: {input_file} ({s}:{container.streams.subtitles[s].name})")
|
67
|
+
for sub2 in container.decode(subtitles=s):
|
68
|
+
if isinstance(sub2, AssSubtitle):
|
69
|
+
print(sub2.ass.decode("utf-8", errors="ignore"))
|
16
70
|
print("------")
|
17
71
|
|
18
72
|
|
auto_editor/cmds/test.py
CHANGED
@@ -11,7 +11,7 @@ from hashlib import sha256
|
|
11
11
|
from tempfile import mkdtemp
|
12
12
|
from time import perf_counter
|
13
13
|
|
14
|
-
import
|
14
|
+
import bv
|
15
15
|
import numpy as np
|
16
16
|
|
17
17
|
from auto_editor.ffwrapper import FileInfo, initFileInfo
|
@@ -73,6 +73,10 @@ def calculate_sha256(filename: str) -> str:
|
|
73
73
|
return sha256_hash.hexdigest()
|
74
74
|
|
75
75
|
|
76
|
+
class SkipTest(Exception):
|
77
|
+
pass
|
78
|
+
|
79
|
+
|
76
80
|
class Runner:
|
77
81
|
def __init__(self) -> None:
|
78
82
|
self.program = [sys.executable, "-m", "auto_editor"]
|
@@ -157,35 +161,35 @@ class Runner:
|
|
157
161
|
file = "resources/testsrc.mp4"
|
158
162
|
out = self.main([file], ["--faststart"]) + ".mp4"
|
159
163
|
fast = calculate_sha256(out)
|
160
|
-
with
|
161
|
-
assert isinstance(container.streams[0],
|
162
|
-
assert isinstance(container.streams[1],
|
164
|
+
with bv.open(out) as container:
|
165
|
+
assert isinstance(container.streams[0], bv.VideoStream)
|
166
|
+
assert isinstance(container.streams[1], bv.AudioStream)
|
163
167
|
|
164
168
|
out = self.main([file], ["--no-faststart"]) + ".mp4"
|
165
169
|
nofast = calculate_sha256(out)
|
166
|
-
with
|
167
|
-
assert isinstance(container.streams[0],
|
168
|
-
assert isinstance(container.streams[1],
|
170
|
+
with bv.open(out) as container:
|
171
|
+
assert isinstance(container.streams[0], bv.VideoStream)
|
172
|
+
assert isinstance(container.streams[1], bv.AudioStream)
|
169
173
|
|
170
174
|
out = self.main([file], ["--fragmented"]) + ".mp4"
|
171
175
|
frag = calculate_sha256(out)
|
172
|
-
with
|
173
|
-
assert isinstance(container.streams[0],
|
174
|
-
assert isinstance(container.streams[1],
|
176
|
+
with bv.open(out) as container:
|
177
|
+
assert isinstance(container.streams[0], bv.VideoStream)
|
178
|
+
assert isinstance(container.streams[1], bv.AudioStream)
|
175
179
|
|
176
180
|
assert fast != nofast, "+faststart is not being applied"
|
177
181
|
assert frag not in (fast, nofast), "fragmented output should diff."
|
178
182
|
|
179
183
|
def test_example(self) -> None:
|
180
184
|
out = self.main(["example.mp4"], [], output="example_ALTERED.mp4")
|
181
|
-
with
|
185
|
+
with bv.open(out) as container:
|
182
186
|
assert container.duration is not None
|
183
187
|
assert container.duration > 17300000 and container.duration < 2 << 24
|
184
188
|
|
185
189
|
video = container.streams[0]
|
186
190
|
audio = container.streams[1]
|
187
|
-
assert isinstance(video,
|
188
|
-
assert isinstance(audio,
|
191
|
+
assert isinstance(video, bv.VideoStream)
|
192
|
+
assert isinstance(audio, bv.AudioStream)
|
189
193
|
assert video.base_rate == 30
|
190
194
|
assert video.average_rate is not None
|
191
195
|
assert video.average_rate == 30, video.average_rate
|
@@ -261,19 +265,19 @@ class Runner:
|
|
261
265
|
self.check([path, "--no-open"], "must have an extension")
|
262
266
|
|
263
267
|
def test_silent_threshold(self):
|
264
|
-
with
|
265
|
-
assert container.duration /
|
268
|
+
with bv.open("resources/new-commentary.mp3") as container:
|
269
|
+
assert container.duration / bv.time_base == 6.732
|
266
270
|
|
267
271
|
out = self.main(
|
268
272
|
["resources/new-commentary.mp3"], ["--edit", "audio:threshold=0.1"]
|
269
273
|
)
|
270
274
|
out += ".mp3"
|
271
275
|
|
272
|
-
with
|
273
|
-
assert container.duration /
|
276
|
+
with bv.open(out) as container:
|
277
|
+
assert container.duration / bv.time_base == 6.552
|
274
278
|
|
275
279
|
def test_track(self):
|
276
|
-
out = self.main(["resources/multi-track.mov"], [
|
280
|
+
out = self.main(["resources/multi-track.mov"], []) + ".mov"
|
277
281
|
assert len(fileinfo(out).audios) == 2
|
278
282
|
|
279
283
|
def test_export_json(self):
|
@@ -357,30 +361,24 @@ class Runner:
|
|
357
361
|
["--edit", "audio:stream=1"],
|
358
362
|
"multi-track_ALTERED.mov",
|
359
363
|
)
|
360
|
-
assert len(fileinfo(out).audios) ==
|
364
|
+
assert len(fileinfo(out).audios) == 2
|
361
365
|
|
362
366
|
def test_concat(self):
|
363
367
|
out = self.main(["example.mp4"], ["--cut-out", "0,171"], "hmm.mp4")
|
364
368
|
self.main(["example.mp4", out], ["--debug"])
|
365
369
|
|
366
370
|
def test_concat_mux_tracks(self):
|
367
|
-
|
368
|
-
|
369
|
-
)
|
371
|
+
inputs = ["example.mp4", "resources/multi-track.mov"]
|
372
|
+
out = self.main(inputs, ["--mix-audio-streams"], "concat_mux.mov")
|
370
373
|
assert len(fileinfo(out).audios) == 1
|
371
374
|
|
372
375
|
def test_concat_multi_tracks(self):
|
373
376
|
out = self.main(
|
374
|
-
["resources/multi-track.mov", "resources/multi-track.mov"],
|
375
|
-
["--keep-tracks-separate"],
|
376
|
-
"out.mov",
|
377
|
+
["resources/multi-track.mov", "resources/multi-track.mov"], [], "out.mov"
|
377
378
|
)
|
378
379
|
assert len(fileinfo(out).audios) == 2
|
379
|
-
|
380
|
-
|
381
|
-
["--keep-tracks-separate"],
|
382
|
-
"out.mov",
|
383
|
-
)
|
380
|
+
inputs = ["example.mp4", "resources/multi-track.mov"]
|
381
|
+
out = self.main(inputs, [], "out.mov")
|
384
382
|
assert len(fileinfo(out).audios) == 2
|
385
383
|
|
386
384
|
def test_frame_rate(self):
|
@@ -464,6 +462,21 @@ class Runner:
|
|
464
462
|
out2 = self.main([out], ["-c:v", "prores"], "prores2.mkv")
|
465
463
|
assert fileinfo(out2).videos[0].pix_fmt == "yuv422p10le"
|
466
464
|
|
465
|
+
def test_decode_hevc(self):
|
466
|
+
out = self.main(["resources/testsrc-hevc.mp4"], ["-c:v", "h264"]) + ".mp4"
|
467
|
+
output = fileinfo(out)
|
468
|
+
assert output.videos[0].codec == "h264"
|
469
|
+
assert output.videos[0].pix_fmt == "yuv420p"
|
470
|
+
|
471
|
+
def test_encode_hevc(self):
|
472
|
+
if os.environ.get("GITHUB_ACTIONS") == "true":
|
473
|
+
raise SkipTest()
|
474
|
+
|
475
|
+
out = self.main(["resources/testsrc.mp4"], ["-c:v", "hevc"], "out.mkv")
|
476
|
+
output = fileinfo(out)
|
477
|
+
assert output.videos[0].codec == "hevc"
|
478
|
+
assert output.videos[0].pix_fmt == "yuv420p"
|
479
|
+
|
467
480
|
# Issue 280
|
468
481
|
def test_SAR(self):
|
469
482
|
out = self.main(["resources/SAR-2by3.mp4"], [], "2by3_out.mp4")
|
@@ -646,16 +659,21 @@ def run_tests(runner: Runner, tests: list[Callable], args: TestArgs) -> None:
|
|
646
659
|
|
647
660
|
def timed_test(test_func):
|
648
661
|
start_time = perf_counter()
|
662
|
+
skipped = False
|
649
663
|
try:
|
650
664
|
test_func()
|
651
665
|
success = True
|
666
|
+
except SkipTest:
|
667
|
+
skipped = True
|
652
668
|
except Exception as e:
|
653
669
|
success = False
|
654
670
|
exception = e
|
655
671
|
end_time = perf_counter()
|
656
672
|
duration = end_time - start_time
|
657
673
|
|
658
|
-
if
|
674
|
+
if skipped:
|
675
|
+
return (SkipTest, duration, None)
|
676
|
+
elif success:
|
659
677
|
return (True, duration, None)
|
660
678
|
else:
|
661
679
|
return (False, duration, exception)
|
@@ -674,17 +692,15 @@ def run_tests(runner: Runner, tests: list[Callable], args: TestArgs) -> None:
|
|
674
692
|
total_time += dur
|
675
693
|
index += 1
|
676
694
|
|
677
|
-
|
695
|
+
msg = f"{name:<26} ({index}/{total}) {round(dur, 2):<5} secs "
|
696
|
+
if success == SkipTest:
|
678
697
|
passed += 1
|
679
|
-
print(
|
680
|
-
|
681
|
-
|
682
|
-
)
|
698
|
+
print(f"{msg}[\033[38;2;125;125;125;mSKIPPED\033[0m]", flush=True)
|
699
|
+
elif success:
|
700
|
+
passed += 1
|
701
|
+
print(f"{msg}[\033[1;32mPASSED\033[0m]", flush=True)
|
683
702
|
else:
|
684
|
-
print(
|
685
|
-
f"{name:<26} ({index}/{total}) {round(dur, 2):<4} secs \033[1;31m[FAILED]\033[0m",
|
686
|
-
flush=True,
|
687
|
-
)
|
703
|
+
print(f"{msg}\033[1;31m[FAILED]\033[0m", flush=True)
|
688
704
|
if args.no_fail_fast:
|
689
705
|
print(f"\n{exception}")
|
690
706
|
else:
|