diff --git a/bin/edl2scenelist.py b/bin/edl2scenelist.py index 48336f5..af3cc64 100644 --- a/bin/edl2scenelist.py +++ b/bin/edl2scenelist.py @@ -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() diff --git a/docs/source/conf.py b/docs/source/conf.py index 3c03a1e..4205076 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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 ------------------------------------------------- diff --git a/src/pycmx/cdl.py b/src/pycmx/cdl.py index 0478220..5eceaa3 100644 --- a/src/pycmx/cdl.py +++ b/src/pycmx/cdl.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing import Generic, NamedTuple, TypeVar -T = TypeVar('T') +T = TypeVar("T") @dataclass diff --git a/src/pycmx/channel_map.py b/src/pycmx/channel_map.py index 542d780..a38aea8 100644 --- a/src/pycmx/channel_map.py +++ b/src/pycmx/channel_map.py @@ -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] diff --git a/src/pycmx/edit.py b/src/pycmx/edit.py index b42a605..ed7de69 100644 --- a/src/pycmx/edit.py +++ b/src/pycmx/edit.py @@ -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 diff --git a/src/pycmx/edit_list.py b/src/pycmx/edit_list.py index 819b5fd..f614303 100644 --- a/src/pycmx/edit_list.py +++ b/src/pycmx/edit_list.py @@ -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: diff --git a/src/pycmx/event.py b/src/pycmx/event.py index 6067c35..9ba6f5d 100644 --- a/src/pycmx/event.py +++ b/src/pycmx/event.py @@ -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) diff --git a/src/pycmx/parse_cmx_statements.py b/src/pycmx/parse_cmx_statements.py index 82a8573..ca43397 100644 --- a/src/pycmx/parse_cmx_statements.py +++ b/src/pycmx/parse_cmx_statements.py @@ -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) diff --git a/src/pycmx/transition.py b/src/pycmx/transition.py index 8913c83..a36c2c0 100644 --- a/src/pycmx/transition.py +++ b/src/pycmx/transition.py @@ -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: diff --git a/src/pycmx/util.py b/src/pycmx/util.py index 3bd0fe0..78ee6d2 100644 --- a/src/pycmx/util.py +++ b/src/pycmx/util.py @@ -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 diff --git a/tests/test_parse.py b/tests/test_parse.py index 967d7a8..15102a4 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -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: