Ruff fixes

This commit is contained in:
2026-09-17 14:49:46 -07:00
parent daedabd7cc
commit 5e560dfa0e
11 changed files with 313 additions and 257 deletions
+52 -30
View File
@@ -5,7 +5,7 @@ import sys
import pycmx
FORMAT = '%(asctime)-15s %(message)s'
FORMAT = "%(asctime)-15s %(message)s"
logging.basicConfig(format=FORMAT)
@@ -36,16 +36,15 @@ def output_cmx(outfile, out_list):
outfile.write("FCM: NON-DROP FRAME\r\n")
for i, o in enumerate(out_list):
line = '%03i AX V C ' % (i)
line += '00:00:00:00 00:00:00:00 %s %s\r\n' % (o['start'], o['end'])
line = "%03i AX V C " % (i)
line += "00:00:00:00 00:00:00:00 %s %s\r\n" % (o["start"], o["end"])
outfile.write(line)
outfile.write("* FROM CLIP NAME: %s\r\n" % (o['scene']))
outfile.write("* FROM CLIP NAME: %s\r\n" % (o["scene"]))
def output_cols(outfile, out_list):
for o in out_list:
outfile.write("%-12s\t%-12s\t%s\n" %
(o['start'], o['end'], o['scene']))
outfile.write("%-12s\t%-12s\t%s\n" % (o["start"], o["end"], o["scene"]))
def scene_list(infile, outfile, out_format, pattern):
@@ -67,15 +66,17 @@ def scene_list(infile, outfile, out_format, pattern):
out_list = []
for group in grouped_edits:
out_list.append({
'start': group[0].record_in,
'end': group[-1].record_out,
'scene': get_scene_name(group[0], pattern)}
out_list.append(
{
"start": group[0].record_in,
"end": group[-1].record_out,
"scene": get_scene_name(group[0], pattern),
}
)
if out_format == 'cmx':
if out_format == "cmx":
output_cmx(outfile, out_list)
if out_format == 'cols':
if out_format == "cols":
output_cols(outfile, out_list)
else:
log.warning(f"Format {out_format} unrecognized. Will use cmx.\n")
@@ -84,28 +85,49 @@ def scene_list(infile, outfile, out_format, pattern):
def scene_list_cli():
parser = argparse.ArgumentParser(
description='Read video events from an input CMX EDL and output '
'events merged into scenes.')
parser.add_argument('-o', '--outfile', default=sys.stdout,
type=argparse.FileType('w'),
help='Output file. Default is stdout.')
parser.add_argument('-f', '--format', default='cmx', type=str,
help='Output format. Options are cols and cmx, cmx '
'is the default.')
parser.add_argument('-p', '--pattern', default='V?([A-Z]*[0-9]+)',
help='RE pattern for extracting scene name from clip '
'name. The default is "V?([A-Z]*[0-9]+)". ' +
'This pattern will be matched case-insensitively.')
parser.add_argument('input_edl', default=sys.stdin,
type=argparse.FileType('r'), nargs='?',
help='Input file. Default is stdin.')
description="Read video events from an input CMX EDL and output "
"events merged into scenes."
)
parser.add_argument(
"-o",
"--outfile",
default=sys.stdout,
type=argparse.FileType("w"),
help="Output file. Default is stdout.",
)
parser.add_argument(
"-f",
"--format",
default="cmx",
type=str,
help="Output format. Options are cols and cmx, cmx is the default.",
)
parser.add_argument(
"-p",
"--pattern",
default="V?([A-Z]*[0-9]+)",
help="RE pattern for extracting scene name from clip "
'name. The default is "V?([A-Z]*[0-9]+)". '
+ "This pattern will be matched case-insensitively.",
)
parser.add_argument(
"input_edl",
default=sys.stdin,
type=argparse.FileType("r"),
nargs="?",
help="Input file. Default is stdin.",
)
args = parser.parse_args()
infile = args.input_edl
scene_list(infile=infile, outfile=args.outfile,
out_format=args.format, pattern=args.pattern)
scene_list(
infile=infile,
outfile=args.outfile,
out_format=args.format,
pattern=args.pattern,
)
if __name__ == '__main__':
if __name__ == "__main__":
scene_list_cli()
+29 -30
View File
@@ -15,14 +15,14 @@ import importlib
import os
import sys
sys.path.insert(0, os.path.abspath('../..'))
sys.path.insert(0, os.path.abspath("../.."))
# -- Project information -----------------------------------------------------
project = 'pycmx'
copyright = '(c) 2018-2025, Jamie Hardt'
author = 'Jamie Hardt'
project = "pycmx"
copyright = "(c) 2018-2025, Jamie Hardt"
author = "Jamie Hardt"
release = importlib.metadata.version("pycmx")
version = release
@@ -40,31 +40,31 @@ version = release
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
"sphinx.ext.autodoc",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx.ext.githubpages",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ".rst"
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = 'em'
language = "em"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@@ -80,18 +80,18 @@ pygments_style = None
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {'collapse_navigation': False}
html_theme_options = {"collapse_navigation": False}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
@@ -107,7 +107,7 @@ html_static_path = ['_static']
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'pycmxdoc'
htmlhelp_basename = "pycmxdoc"
# -- Options for LaTeX output ------------------------------------------------
@@ -116,15 +116,12 @@ latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
@@ -134,8 +131,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'pycmx.tex', 'pycmx Documentation',
'Jamie Hardt', 'manual'),
(master_doc, "pycmx.tex", "pycmx Documentation", "Jamie Hardt", "manual"),
]
@@ -143,10 +139,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pycmx', 'pycmx Documentation',
[author], "3p")
]
man_pages = [(master_doc, "pycmx", "pycmx Documentation", [author], "3p")]
# -- Options for Texinfo output ----------------------------------------------
@@ -155,9 +148,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'pycmx', 'pycmx Documentation',
author, 'pycmx', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
"pycmx",
"pycmx Documentation",
author,
"pycmx",
"One line description of project.",
"Miscellaneous",
),
]
@@ -176,7 +175,7 @@ epub_title = project
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------
+1 -1
View File
@@ -4,7 +4,7 @@
from dataclasses import dataclass
from typing import Generic, NamedTuple, TypeVar
T = TypeVar('T')
T = TypeVar("T")
@dataclass
+18 -18
View File
@@ -1,8 +1,10 @@
# pycmx
# (c) 2018-2025 Jamie Hardt
from __future__ import annotations
from re import compile, match
from typing import Dict, Generator, Tuple
from typing import Generator
class ChannelMap:
@@ -10,35 +12,33 @@ class ChannelMap:
Represents a set of all the channels to which an event applies.
"""
_chan_map: Dict[str, Tuple] = {
"V": (True, False, False),
"A": (False, True, False),
"A2": (False, False, True),
"AA": (False, True, True),
"B": (True, True, False),
"AA/V": (True, True, True),
"A2/V": (True, False, True)
}
def __init__(self, v=False, audio_channels=set()):
self._audio_channel_set = audio_channels
self.v = v
self._chan_map: dict[str, tuple] = {
"V": (True, False, False),
"A": (False, True, False),
"A2": (False, False, True),
"AA": (False, True, True),
"B": (True, True, False),
"AA/V": (True, True, True),
"A2/V": (True, False, True),
}
@property
def video(self) -> bool:
'True if video is included'
"True if video is included"
return self.v
@property
def audio(self) -> bool:
'True if an audio channel is included'
"True if an audio channel is included"
return len(self._audio_channel_set) > 0
@property
def channels(self) -> Generator[int, None, None]:
'A generator for each audio channel'
for c in self._audio_channel_set:
yield c
"A generator for each audio channel"
yield from self._audio_channel_set
@property
def a1(self) -> bool:
@@ -78,7 +78,7 @@ class ChannelMap:
def get_audio_channel(self, chan_num) -> bool:
"""True if chan_num is included"""
return (chan_num in self._audio_channel_set)
return chan_num in self._audio_channel_set
def set_audio_channel(self, chan_num, enabled: bool):
"""If enabled is true, chan_num will be included"""
@@ -88,7 +88,7 @@ class ChannelMap:
self._audio_channel_set.remove(chan_num)
def _append_event(self, event_str):
alt_channel_re = compile(r'^A(\d+)')
alt_channel_re = compile(r"^A(\d+)")
if event_str in self._chan_map:
channels = self._chan_map[event_str]
self.v = channels[0]
+21 -23
View File
@@ -1,7 +1,7 @@
# pycmx
# (c) 2018-2025 Jamie Hardt
from typing import Optional
from __future__ import annotations
from .cdl import AscSopComponents, FramecountTriple
from .channel_map import ChannelMap
@@ -27,24 +27,22 @@ class Edit:
def __init__(
self,
edit_statement: StmtEvent,
audio_ext_statement: Optional[StmtAudioExt],
clip_name_statement: Optional[StmtClipName],
source_file_statement: Optional[StmtSourceFile],
trans_name_statement: Optional[StmtEffectsName] = None,
asc_sop_statement: Optional[StmtCdlSop] = None,
asc_sat_statement: Optional[StmtCdlSat] = None,
frmc_statement: Optional[StmtFrmc] = None,
audio_ext_statement: StmtAudioExt | None,
clip_name_statement: StmtClipName | None,
source_file_statement: StmtSourceFile | None,
trans_name_statement: StmtEffectsName | None = None,
asc_sop_statement: StmtCdlSop | None = None,
asc_sat_statement: StmtCdlSat | None = None,
frmc_statement: StmtFrmc | None = None,
) -> None:
self._edit_statement: StmtEvent = edit_statement
self._audio_ext: Optional[StmtAudioExt] = audio_ext_statement
self._clip_name_statement: Optional[StmtClipName] = clip_name_statement
self._source_file_statement: Optional[StmtSourceFile] = \
source_file_statement
self._trans_name_statement: Optional[StmtEffectsName] = \
trans_name_statement
self._asc_sop_statement: Optional[StmtCdlSop] = asc_sop_statement
self._asc_sat_statement: Optional[StmtCdlSat] = asc_sat_statement
self._frmc_statement: Optional[StmtFrmc] = frmc_statement
self._audio_ext: StmtAudioExt | None = audio_ext_statement
self._clip_name_statement: StmtClipName | None = clip_name_statement
self._source_file_statement: StmtSourceFile | None = source_file_statement
self._trans_name_statement: StmtEffectsName | None = trans_name_statement
self._asc_sop_statement: StmtCdlSop | None = asc_sop_statement
self._asc_sat_statement: StmtCdlSat | None = asc_sat_statement
self._frmc_statement: StmtFrmc | None = frmc_statement
@property
def line_number(self) -> int:
@@ -138,7 +136,7 @@ class Edit:
return self.source == "AX"
@property
def source_file(self) -> Optional[str]:
def source_file(self) -> str | None:
"""
Get the source file, as attested by a "* SOURCE FILE" remark on the
EDL. This will return None if the information is not present.
@@ -149,7 +147,7 @@ class Edit:
return self._source_file_statement.filename
@property
def clip_name(self) -> Optional[str]:
def clip_name(self) -> str | None:
"""
Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP
NAME" remark on the EDL. This will return None if the information is
@@ -161,7 +159,7 @@ class Edit:
return self._clip_name_statement.name
@property
def asc_sop(self) -> Optional[AscSopComponents[float]]:
def asc_sop(self) -> AscSopComponents[float] | None:
"""
Get ASC CDL Slope-Offset-Power color transfer function for the edit,
if present. The ASC SOP is a transfer function of the form:
@@ -177,7 +175,7 @@ class Edit:
return self._asc_sop_statement.cdl_sop
@property
def asc_sop_raw(self) -> Optional[str]:
def asc_sop_raw(self) -> str | None:
"""
ASC CDL Slope-Offset-Power statement raw line.
"""
@@ -187,7 +185,7 @@ class Edit:
return self._asc_sop_statement.line
@property
def asc_sat(self) -> Optional[float]:
def asc_sat(self) -> float | None:
"""
Get ASC CDL saturation value for clip, if present.
"""
@@ -197,7 +195,7 @@ class Edit:
return self._asc_sat_statement.value
@property
def framecounts(self) -> Optional[FramecountTriple]:
def framecounts(self) -> FramecountTriple | None:
"""
Get frame count offset data, if it exists. If an FRMC statement exists
in the EDL for the event it will give an integer frame count for the
+11 -8
View File
@@ -1,6 +1,8 @@
# pycmx
# (c) 2018-2025 Jamie Hardt
from __future__ import annotations
from typing import Any, Generator
from .channel_map import ChannelMap
@@ -33,19 +35,20 @@ class EditList:
Adobe EDLs with more than 999 events will be reported as "3600".
"""
first_event = next(
(s for s in self.event_statements if type(s) is StmtEvent), None)
(s for s in self.event_statements if type(s) is StmtEvent), None
)
if first_event:
if first_event.source_field_size == 8:
return '3600'
return "3600"
elif first_event.source_field_size == 32:
return 'File32'
return "File32"
elif first_event.source_field_size == 128:
return 'File128'
return "File128"
else:
return 'unknown'
return "unknown"
else:
return 'unknown'
return "unknown"
@property
def channels(self) -> ChannelMap:
@@ -77,12 +80,12 @@ class EditList:
:class:`StmtCorruptRemark`
"""
for s in self.event_statements:
if type(s) is StmtUnrecognized or type(s) in StmtCorruptRemark:
if type(s) is StmtUnrecognized or type(s) is StmtCorruptRemark:
yield s
@property
def events(self) -> Generator[Event, None, None]:
'A generator for all the events in the edit list'
"A generator for all the events in the edit list"
current_event_num = None
event_statements = []
for stmt in self.event_statements:
+23 -21
View File
@@ -20,7 +20,7 @@ from .statements import (
class Event:
"""
Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same
event number. """
event number."""
def __init__(self, statements):
self.statements = statements
@@ -65,9 +65,9 @@ class Event:
end_name: Optional[StmtClipName] = None
for clip_name in clip_names:
if clip_name.affect == 'from':
if clip_name.affect == "from":
start_name = clip_name
elif clip_name.affect == 'to':
elif clip_name.affect == "to":
end_name = clip_name
the_zip.append([start_name, end_name])
@@ -97,19 +97,22 @@ class Event:
except IndexError:
the_zip.append([None] * len(edits_audio))
return [Edit(edit_statement=e1[0],
audio_ext_statement=e1[1],
clip_name_statement=n1,
source_file_statement=s1,
trans_name_statement=u1,
asc_sop_statement=self._asc_sop_statement(),
asc_sat_statement=self._asc_sat_statement(),
frmc_statement=self._frmc_statement())
for (e1, n1, s1, u1) in zip(*the_zip)]
return [
Edit(
edit_statement=e1[0],
audio_ext_statement=e1[1],
clip_name_statement=n1,
source_file_statement=s1,
trans_name_statement=u1,
asc_sop_statement=self._asc_sop_statement(),
asc_sat_statement=self._asc_sat_statement(),
frmc_statement=self._frmc_statement(),
)
for (e1, n1, s1, u1) in zip(*the_zip)
]
@property
def unrecognized_statements(self) -> Generator[StmtUnrecognized, None,
None]:
def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]:
"""
A generator for all the unrecognized statements in the event.
"""
@@ -129,26 +132,25 @@ class Event:
def _source_file_statements(self) -> List[StmtSourceFile]:
return [s for s in self.statements if type(s) is StmtSourceFile]
def _statements_with_audio_ext(self) -> Generator[
Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
def _statements_with_audio_ext(
self,
) -> Generator[Tuple[StmtEvent, Optional[StmtAudioExt]], None, None]:
if len(self.statements) == 1 and type(self.statements[0]) is StmtEvent:
yield (self.statements[0], None)
else:
for (s1, s2) in zip(self.statements, self.statements[1:]):
for s1, s2 in zip(self.statements, self.statements[1:]):
if type(s1) is StmtEvent and type(s2) is StmtAudioExt:
yield (s1, s2)
elif type(s1) is StmtEvent:
yield (s1, None)
def _asc_sop_statement(self) -> Optional[StmtCdlSop]:
return next((s for s in self.statements if type(s) is StmtCdlSop),
None)
return next((s for s in self.statements if type(s) is StmtCdlSop), None)
def _asc_sat_statement(self) -> Optional[StmtCdlSat]:
return next((s for s in self.statements if type(s) is StmtCdlSat),
None)
return next((s for s in self.statements if type(s) is StmtCdlSat), None)
def _frmc_statement(self) -> Optional[StmtFrmc]:
return next((s for s in self.statements if type(s) is StmtFrmc), None)
+116 -78
View File
@@ -24,25 +24,38 @@ from .statements import (
from .util import collimate
def parse_cmx3600_statements(file: TextIO,
tolerant: bool = False) -> List[object]:
def parse_cmx3600_statements(file: TextIO, tolerant: bool = False) -> List[object]:
"""
Return a list of every statement in the file argument.
"""
lines = file.readlines()
return [_parse_cmx3600_line(line.strip(), line_number, tolerant)
for (line_number, line) in enumerate(lines)]
return [
_parse_cmx3600_line(line.strip(), line_number, tolerant)
for (line_number, line) in enumerate(lines)
]
def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
return [event_field_length, 2, source_field_length, 1,
4, 2, # chans
4, 1, # trans
3, 1, # trans op
11, 1,
11, 1,
11, 1,
11]
return [
event_field_length,
2,
source_field_length,
1,
4,
2, # chans
4,
1, # trans
3,
1, # trans op
11,
1,
11,
1,
11,
1,
11,
]
# def _edl_m2_column_widths():
# return [2, # "M2"
@@ -50,8 +63,7 @@ def _edl_column_widths(event_field_length, source_field_length) -> List[int]:
# 8,8,1,4,2,1,4,13,3,1,1]
def _parse_cmx3600_line(line: str, line_number: int,
tolerant: bool = False) -> object:
def _parse_cmx3600_line(line: str, line_number: int, tolerant: bool = False) -> object:
"""
Parses a single CMX EDL line.
@@ -72,7 +84,8 @@ def _parse_cmx3600_line(line: str, line_number: int,
try:
return _parse_columns_for_standard_form(
line, event_field_len, source_field_len, line_number)
line, event_field_len, source_field_len, line_number
)
except EventFormError:
if tolerant:
@@ -123,19 +136,21 @@ def _parse_extended_audio_channels(line, line_number):
def _parse_remark(line, line_number) -> object:
if line.startswith("FROM CLIP NAME:"):
return StmtClipName(name=line[15:].strip(), affect="from",
line_number=line_number)
return StmtClipName(
name=line[15:].strip(), affect="from", line_number=line_number
)
elif line.startswith("TO CLIP NAME:"):
return StmtClipName(name=line[13:].strip(), affect="to",
line_number=line_number)
return StmtClipName(
name=line[13:].strip(), affect="to", line_number=line_number
)
elif line.startswith("SOURCE FILE:"):
return StmtSourceFile(filename=line[12:].strip(),
line_number=line_number)
return StmtSourceFile(filename=line[12:].strip(), line_number=line_number)
elif line.startswith("ASC_SOP"):
group_patterns: list[str] = re.findall(r'\((.*?)\)', line)
group_patterns: list[str] = re.findall(r"\((.*?)\)", line)
v1: list[list[tuple[str, str]]] = \
[re.findall(r'(-?\d+(\.\d+)?)', a) for a in group_patterns]
v1: list[list[tuple[str, str]]] = [
re.findall(r"(-?\d+(\.\d+)?)", a) for a in group_patterns
]
v: list[list[str]] = [[a[0] for a in b] for b in v1]
@@ -144,52 +159,65 @@ def _parse_remark(line, line_number) -> object:
else:
try:
return StmtCdlSop(line=line,
cdl_sop=AscSopComponents(
slope=Rgb(red=float(v[0][0]),
green=float(v[0][1]),
blue=float(v[0][2])),
offset=Rgb(red=float(v[1][0]),
green=float(v[1][1]),
blue=float(v[1][2])),
power=Rgb(red=float(v[2][0]),
green=float(v[2][1]),
blue=float(v[2][2]))
),
line_number=line_number)
return StmtCdlSop(
line=line,
cdl_sop=AscSopComponents(
slope=Rgb(
red=float(v[0][0]),
green=float(v[0][1]),
blue=float(v[0][2]),
),
offset=Rgb(
red=float(v[1][0]),
green=float(v[1][1]),
blue=float(v[1][2]),
),
power=Rgb(
red=float(v[2][0]),
green=float(v[2][1]),
blue=float(v[2][2]),
),
),
line_number=line_number,
)
except ValueError as e:
return StmtCorruptRemark('ASC_SOP', e, line_number)
return StmtCorruptRemark("ASC_SOP", e, line_number)
elif line.startswith("ASC_SAT"):
value = re.findall(r'(-?\d+(\.\d+)?)', line)
value = re.findall(r"(-?\d+(\.\d+)?)", line)
if len(value) != 1:
return StmtRemark(line, line_number)
else:
try:
return StmtCdlSat(value=float(value[0][0]),
line_number=line_number)
return StmtCdlSat(value=float(value[0][0]), line_number=line_number)
except ValueError as e:
return StmtCorruptRemark('ASC_SAT', e, line_number)
return StmtCorruptRemark("ASC_SAT", e, line_number)
elif line.startswith("FRMC"):
match = re.match(r'^FRMC START:\s*(\d+)\s+FRMC END:\s*(\d+)'
r'\s+FRMC DURATION:\s*(\d+)', line, re.IGNORECASE)
match = re.match(
r"^FRMC START:\s*(\d+)\s+FRMC END:\s*(\d+)"
r"\s+FRMC DURATION:\s*(\d+)",
line,
re.IGNORECASE,
)
if match is None:
return StmtCorruptRemark('FRMC', None, line_number)
return StmtCorruptRemark("FRMC", None, line_number)
else:
try:
return StmtFrmc(start=int(match.group(1)),
end=int(match.group(2)),
duration=int(match.group(3)),
line_number=line_number)
return StmtFrmc(
start=int(match.group(1)),
end=int(match.group(2)),
duration=int(match.group(3)),
line_number=line_number,
)
except ValueError as e:
return StmtCorruptRemark('FRMC', e, line_number)
return StmtCorruptRemark("FRMC", e, line_number)
else:
return StmtRemark(text=line, line_number=line_number)
@@ -205,14 +233,14 @@ def _parse_split(line: str, line_number):
is_video = split_type.startswith("VIDEO")
split_delay = line[24:35]
return StmtSplitEdit(video=is_video, delay=split_delay,
line_number=line_number)
return StmtSplitEdit(video=is_video, delay=split_delay, line_number=line_number)
# def _parse_motion_memory(line, line_number):
# return StmtMotionMemory(source="", fps="")
#
class EventFormError(RuntimeError):
pass
@@ -221,9 +249,9 @@ def _parse_unrecognized(line, line_number):
return StmtUnrecognized(content=line, line_number=line_number)
def _parse_columns_for_standard_form(line: str, event_field_length: int,
source_field_length: int,
line_number: int):
def _parse_columns_for_standard_form(
line: str, event_field_length: int, source_field_length: int, line_number: int
):
# breakpoint()
col_widths = _edl_column_widths(event_field_length, source_field_length)
@@ -238,35 +266,45 @@ def _parse_columns_for_standard_form(line: str, event_field_length: int,
if len(channels) == 0 or len(trans) == 0:
raise EventFormError()
return StmtEvent(event=column_strings[0],
source=column_strings[2].strip(),
channels=channels,
trans=trans,
trans_op=column_strings[8].strip(),
source_in=column_strings[10].strip(),
source_out=column_strings[12].strip(),
record_in=column_strings[14].strip(),
record_out=column_strings[16].strip(),
line_number=line_number,
source_field_size=source_field_length)
return StmtEvent(
event=column_strings[0],
source=column_strings[2].strip(),
channels=channels,
trans=trans,
trans_op=column_strings[8].strip(),
source_in=column_strings[10].strip(),
source_out=column_strings[12].strip(),
record_in=column_strings[14].strip(),
record_out=column_strings[16].strip(),
line_number=line_number,
source_field_size=source_field_length,
)
def _parse_columns_tolerant(line: str, line_number: int):
pattern = re.compile(r'^\s*(\d+)\s+(.{8,128}?)\s+'
r'(V|A|A2|AA|NONE|AA/V|A2/V|B)\s+'
r'(C|D|W|KB|K|KO)\s+(\d*)\s+(\d\d.\d\d.\d\d.\d\d)\s'
r'(\d\d.\d\d.\d\d.\d\d)\s(\d\d.\d\d.\d\d.\d\d)\s'
r'(\d\d.\d\d.\d\d.\d\d)'
)
pattern = re.compile(
r"^\s*(\d+)\s+(.{8,128}?)\s+"
r"(V|A|A2|AA|NONE|AA/V|A2/V|B)\s+"
r"(C|D|W|KB|K|KO)\s+(\d*)\s+(\d\d.\d\d.\d\d.\d\d)\s"
r"(\d\d.\d\d.\d\d.\d\d)\s(\d\d.\d\d.\d\d.\d\d)\s"
r"(\d\d.\d\d.\d\d.\d\d)"
)
match = pattern.match(line)
if match:
return StmtEvent(event=int(match.group(1)), source=match.group(2),
channels=match.group(3), trans=match.group(4),
trans_op=match.group(5), source_in=match.group(6),
source_out=match.group(7), record_in=match.group(8),
record_out=match.group(9), line_number=line_number,
source_field_size=len(match.group(2)))
return StmtEvent(
event=int(match.group(1)),
source=match.group(2),
channels=match.group(3),
trans=match.group(4),
trans_op=match.group(5),
source_in=match.group(6),
source_out=match.group(7),
record_in=match.group(8),
record_out=match.group(9),
line_number=line_number,
source_field_size=len(match.group(2)),
)
else:
return StmtUnrecognized(line, line_number)
+3 -3
View File
@@ -42,17 +42,17 @@ class Transition:
@property
def cut(self) -> bool:
"`True` if this transition is a cut."
return self.transition == 'C'
return self.transition == "C"
@property
def dissolve(self) -> bool:
"`True` if this traansition is a dissolve."
return self.transition == 'D'
return self.transition == "D"
@property
def wipe(self) -> bool:
"`True` if this transition is a wipe."
return self.transition.startswith('W')
return self.transition.startswith("W")
@property
def effect_duration(self) -> int:
+1
View File
@@ -3,6 +3,7 @@
# Utility functions
def collimate(a_string, column_widths):
"""
Split a list-type thing, like a string, into slices that are column_widths
+38 -45
View File
@@ -4,31 +4,32 @@ import pycmx
class TestParse(TestCase):
files = ["INS4_R1_010417.edl",
"INS4_R1_DX_092117.edl",
"STP R1 v082517.edl",
"ToD_R4_LOCK3.1_030618_Video.edl",
"TEST.edl",
"test_edl_cdl.edl",
"INS4_R1_DX_092117.edl"
]
files = [
"INS4_R1_010417.edl",
"INS4_R1_DX_092117.edl",
"STP R1 v082517.edl",
"ToD_R4_LOCK3.1_030618_Video.edl",
"TEST.edl",
"test_edl_cdl.edl",
"INS4_R1_DX_092117.edl",
]
def test_event_counts(self):
counts = [287, 466, 250, 376, 120, 3, 466]
for fn, count in zip(type(self).files, counts):
with open("tests/edls/" + fn, 'r') as f:
with open("tests/edls/" + fn, "r") as f:
edl = pycmx.parse_cmx3600(f)
actual = len(list(edl.events))
self.assertTrue(actual == count,
"expected %i in file %s but found %i"
% (count, fn, actual))
self.assertTrue(
actual == count,
"expected %i in file %s but found %i" % (count, fn, actual),
)
def test_list_sanity(self):
for fn in type(self).files:
with open("tests/edls/" + fn, 'r') as f:
with open("tests/edls/" + fn, "r") as f:
edl = pycmx.parse_cmx3600(f)
self.assertTrue(type(edl.title) is str)
self.assertTrue(len(edl.title) > 0)
@@ -36,33 +37,29 @@ class TestParse(TestCase):
def test_event_sanity(self):
for fn in type(self).files:
path = "tests/edls/" + fn
with open(path, 'r') as f:
with open(path, "r") as f:
edl = pycmx.parse_cmx3600(f)
for index, event in enumerate(edl.events):
self.assertTrue(len(event.edits) > 0,
f"Failed for {path}")
self.assertEqual(event.number, index + 1,
f"Failed for {path}")
self.assertTrue(len(event.edits) > 0, f"Failed for {path}")
self.assertEqual(event.number, index + 1, f"Failed for {path}")
def test_events(self):
with open("tests/edls/TEST.edl", 'r') as f:
with open("tests/edls/TEST.edl", "r") as f:
edl = pycmx.parse_cmx3600(f)
events = list(edl.events)
self.assertEqual(events[0].number, 1)
self.assertEqual(events[0].edits[0].source, "OY_HEAD_")
self.assertEqual(events[0].edits[0].clip_name, "HEAD LEADER MONO")
self.assertEqual(
events[0].edits[0].source_file, "OY_HEAD_LEADER.MOV")
self.assertEqual(events[0].edits[0].source_file, "OY_HEAD_LEADER.MOV")
self.assertEqual(events[0].edits[0].source_in, "00:00:00:00")
self.assertEqual(events[0].edits[0].source_out, "00:00:00:00")
self.assertEqual(events[0].edits[0].record_in, "01:00:00:00")
self.assertEqual(events[0].edits[0].record_out, "01:00:08:00")
self.assertTrue(
events[0].edits[0].transition.kind == pycmx.Transition.Cut)
self.assertTrue(events[0].edits[0].transition.kind == pycmx.Transition.Cut)
def test_channel_map(self):
with open("tests/edls/TEST.edl", 'r') as f:
with open("tests/edls/TEST.edl", "r") as f:
edl = pycmx.parse_cmx3600(f)
events = list(edl.events)
self.assertFalse(events[0].edits[0].channels.video)
@@ -72,7 +69,7 @@ class TestParse(TestCase):
self.assertTrue(events[2].edits[0].channels.audio)
def test_multi_edit_events(self):
with open("tests/edls/TEST.edl", 'r') as f:
with open("tests/edls/TEST.edl", "r") as f:
edl = pycmx.parse_cmx3600(f)
events = list(edl.events)
@@ -80,25 +77,24 @@ class TestParse(TestCase):
self.assertEqual(len(events[42].edits), 2)
self.assertEqual(events[42].edits[0].source, "TC_R1_V1")
self.assertEqual(events[42].edits[0].clip_name,
"TC R1 V1.2 TEMP1 FX ST.WAV")
self.assertEqual(
events[42].edits[0].clip_name, "TC R1 V1.2 TEMP1 FX ST.WAV"
)
self.assertEqual(events[42].edits[0].source_in, "00:00:00:00")
self.assertEqual(events[42].edits[0].source_out, "00:00:00:00")
self.assertEqual(events[42].edits[0].record_in, "01:08:56:09")
self.assertEqual(events[42].edits[0].record_out, "01:08:56:09")
self.assertTrue(
events[42].edits[0].transition.kind == pycmx.Transition.Cut)
self.assertTrue(events[42].edits[0].transition.kind == pycmx.Transition.Cut)
self.assertEqual(events[42].edits[1].source, "TC_R1_V6")
self.assertEqual(events[42].edits[1].clip_name,
"TC R1 V6 TEMP2 ST FX.WAV")
self.assertEqual(events[42].edits[1].clip_name, "TC R1 V6 TEMP2 ST FX.WAV")
self.assertEqual(events[42].edits[1].source_in, "00:00:00:00")
self.assertEqual(events[42].edits[1].source_out, "00:00:00:00")
self.assertEqual(events[42].edits[1].record_in, "01:08:56:09")
self.assertEqual(events[42].edits[1].record_out, "01:08:56:11")
self.assertTrue(
events[42].edits[1].transition.kind ==
pycmx.Transition.Dissolve)
events[42].edits[1].transition.kind == pycmx.Transition.Dissolve
)
def test_line_numbers(self):
with open("tests/edls/ToD_R4_LOCK3.1_030618_Video.edl") as f:
@@ -113,12 +109,10 @@ class TestParse(TestCase):
with open("tests/edls/test_25.edl", "r") as f:
edl = pycmx.parse_cmx3600(f)
events = list(edl.events)
self.assertEqual(
events[4].edits[1].transition.name, "CROSS DISSOLVE")
self.assertEqual(events[4].edits[1].transition.name, "CROSS DISSOLVE")
def test_adobe_wide(self):
with open("tests/edls/adobe_dai109_test.txt", 'r',
encoding='ISO-8859-1') as f:
with open("tests/edls/adobe_dai109_test.txt", "r", encoding="ISO-8859-1") as f:
edl = pycmx.parse_cmx3600(f)
events = list(edl.events)
@@ -131,12 +125,11 @@ class TestParse(TestCase):
for event in edl.events:
if event.number == 42:
self.assertEqual(len(event.edits), 1)
self.assertEqual(event.edits[0].source,
"M018C0005_240925_1F4L13")
self.assertEqual(event.edits[0].transition.kind,
pycmx.Transition.Cut)
self.assertEqual(event.edits[0].source_in,
"18:44:20:12")
self.assertEqual(event.edits[0].source, "M018C0005_240925_1F4L13")
self.assertEqual(
event.edits[0].transition.kind, pycmx.Transition.Cut
)
self.assertEqual(event.edits[0].source_in, "18:44:20:12")
def test_cdl(self):
with open("tests/edls/cdl_example01.edl", "r") as f:
@@ -152,7 +145,7 @@ class TestParse(TestCase):
sat = event.edits[0].asc_sat
self.assertIsNotNone(sat)
assert sat
self.assertEqual(sat, float('0.9640'))
self.assertEqual(sat, float("0.9640"))
def test_frmc(self):
with open("tests/edls/cdl_frmc_example01.edl", "r") as f: