diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..b3fbee1 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +per-file-ignores = + pycmx/__init__.py: F401 + tests/__init__.py: F401 + diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bff4c78..e408048 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -32,8 +32,7 @@ jobs: run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 . --count --max-line-length=79 --statistics - name: Test with pytest run: | pytest diff --git a/bin/edl2scenelist.py b/bin/edl2scenelist.py index 877b689..54cbbac 100644 --- a/bin/edl2scenelist.py +++ b/bin/edl2scenelist.py @@ -12,6 +12,7 @@ logging.basicConfig(format=FORMAT) log = logging.getLogger(__name__) + def all_video_edits(edl): for event in edl.events: for edit in event.edits: @@ -30,46 +31,49 @@ def get_scene_name(edit, pattern): else: return edit.clip_name + def output_cmx(outfile, out_list): outfile.write("TITLE: SCENE LIST\r\n") outfile.write("FCM: NON-DROP FRAME\r\n") - for o in out_list: - line = "%03i AX V C 00:00:00:00 00:00:00:00 %s %s\r\n" % (0, o['start'],o['end']) + 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']) 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): - + edl = pycmx.parse_cmx3600(infile) current_scene_name = None - - grouped_edits = [ ] + + grouped_edits = [] for edit in all_video_edits(edl): this_scene_name = get_scene_name(edit, pattern) if this_scene_name is not None: if current_scene_name != this_scene_name: - grouped_edits.append([ ]) + grouped_edits.append([]) current_scene_name = this_scene_name grouped_edits[-1].append(edit) - - out_list = [ ] + + out_list = [] for group in grouped_edits: - out_list.append({ - 'start': group[0].record_in, + out_list.append({ + 'start': group[0].record_in, 'end': group[-1].record_out, - 'scene': get_scene_name(group[0], pattern ) } - ) - + 'scene': get_scene_name(group[0], pattern)} + ) + if out_format == 'cmx': output_cmx(outfile, out_list) if out_format == 'cols': @@ -80,23 +84,29 @@ 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.') + 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.') 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__': - scene_list_cli() + scene_list_cli() diff --git a/docs/source/conf.py b/docs/source/conf.py index a530be3..06e2e69 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,22 +12,20 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # + import os import sys sys.path.insert(0, os.path.abspath('../..')) -import pycmx # -- Project information ----------------------------------------------------- project = u'pycmx' -copyright = u'(c) 2023, Jamie Hardt' +copyright = u'(c) 2025, Jamie Hardt' author = u'Jamie Hardt' # The short X.Y version -version = pycmx.__version__ # The full version, including alpha/beta/rc tags -release = pycmx.__version__ # -- General configuration --------------------------------------------------- diff --git a/pycmx/__init__.py b/pycmx/__init__.py index fde3f9c..2877325 100644 --- a/pycmx/__init__.py +++ b/pycmx/__init__.py @@ -2,13 +2,11 @@ """ pycmx is a parser for CMX 3600-style EDLs. -This module (c) 2023 Jamie Hardt. For more information on your rights to +This module (c) 2025 Jamie Hardt. For more information on your rights to copy and reuse this software, refer to the LICENSE file included with the distribution. """ -__version__ = '1.2.2' - from .parse_cmx_events import parse_cmx3600 from .transition import Transition from .event import Event diff --git a/pycmx/channel_map.py b/pycmx/channel_map.py index f52cc4f..a8afea0 100644 --- a/pycmx/channel_map.py +++ b/pycmx/channel_map.py @@ -4,23 +4,24 @@ from re import (compile, match) from typing import Dict, Tuple, Generator + 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) - } + _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._audio_channel_set = audio_channels self.v = v @property @@ -46,7 +47,7 @@ class ChannelMap: @a1.setter def a1(self, val: bool): - self.set_audio_channel(1,val) + self.set_audio_channel(1, val) @property def a2(self) -> bool: @@ -55,7 +56,7 @@ class ChannelMap: @a2.setter def a2(self, val: bool): - self.set_audio_channel(2,val) + self.set_audio_channel(2, val) @property def a3(self) -> bool: @@ -64,28 +65,28 @@ class ChannelMap: @a3.setter def a3(self, val: bool): - self.set_audio_channel(3,val) - + self.set_audio_channel(3, val) + @property def a4(self) -> bool: """True if A4 is included""" return self.get_audio_channel(4) @a4.setter - def a4(self,val: bool): - self.set_audio_channel(4,val) + def a4(self, val: bool): + self.set_audio_channel(4, val) def get_audio_channel(self, chan_num) -> bool: """True if chan_num is included""" return (chan_num in self._audio_channel_set) - def set_audio_channel(self,chan_num, enabled: bool): + def set_audio_channel(self, chan_num, enabled: bool): """If enabled is true, chan_num will be included""" if enabled: self._audio_channel_set.add(chan_num) elif self.get_audio_channel(chan_num): self._audio_channel_set.remove(chan_num) - + def _append_event(self, event_str): alt_channel_re = compile(r'^A(\d+)') if event_str in self._chan_map: @@ -96,7 +97,7 @@ class ChannelMap: else: matchresult = match(alt_channel_re, event_str) if matchresult: - self.set_audio_channel(int( matchresult.group(1)), True ) + self.set_audio_channel(int(matchresult.group(1)), True) def _append_ext(self, audio_ext): self.a3 = audio_ext.audio3 @@ -109,5 +110,4 @@ class ChannelMap: out_v = self.video | other.video out_a = self._audio_channel_set | other._audio_channel_set - return ChannelMap(v=out_v,audio_channels = out_a) - + return ChannelMap(v=out_v, audio_channels=out_a) diff --git a/pycmx/edit.py b/pycmx/edit.py index 3f60b3c..7dafb64 100644 --- a/pycmx/edit.py +++ b/pycmx/edit.py @@ -7,23 +7,27 @@ from .channel_map import ChannelMap from typing import Optional + class Edit: """ - An individual source-to-record operation, with a source roll, source and + An individual source-to-record operation, with a source roll, source and recorder timecode in and out, a transition and channels. """ - def __init__(self, edit_statement, audio_ext_statement, clip_name_statement, source_file_statement, trans_name_statement = None): + + def __init__(self, edit_statement, audio_ext_statement, + clip_name_statement, source_file_statement, + trans_name_statement=None): self.edit_statement = edit_statement self.audio_ext = audio_ext_statement self.clip_name_statement = clip_name_statement self.source_file_statement = source_file_statement - self.trans_name_statement = trans_name_statement + self.trans_name_statement = trans_name_statement @property def line_number(self) -> int: """ Get the line number for the "standard form" statement associated with - this edit. Line numbers a zero-indexed, such that the + this edit. Line numbers a zero-indexed, such that the "TITLE:" record is line zero. """ return self.edit_statement.line_number @@ -35,7 +39,7 @@ class Edit: """ cm = ChannelMap() cm._append_event(self.edit_statement.channels) - if self.audio_ext != None: + if self.audio_ext is not None: cm._append_ext(self.audio_ext) return cm @@ -45,10 +49,13 @@ class Edit: Get the :obj:`Transition` object associated with this edit. """ if self.trans_name_statement: - return Transition(self.edit_statement.trans, self.edit_statement.trans_op, self.trans_name_statement.name) + return Transition(self.edit_statement.trans, + self.edit_statement.trans_op, + self.trans_name_statement.name) else: - return Transition(self.edit_statement.trans, self.edit_statement.trans_op, None) - + return Transition(self.edit_statement.trans, + self.edit_statement.trans_op, None) + @property def source_in(self) -> str: """ @@ -116,7 +123,7 @@ class Edit: @property def clip_name(self) -> Optional[str]: """ - Get the clip name, as attested by a "* FROM CLIP NAME" or "* TO CLIP + 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 not present. """ @@ -124,5 +131,3 @@ class Edit: return None else: return self.clip_name_statement.name - - diff --git a/pycmx/edit_list.py b/pycmx/edit_list.py index 1dd6067..08e6c39 100644 --- a/pycmx/edit_list.py +++ b/pycmx/edit_list.py @@ -1,16 +1,20 @@ # pycmx # (c) 2018 Jamie Hardt -from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSourceUMID) +from .parse_cmx_statements import ( + StmtUnrecognized, StmtEvent, StmtSourceUMID) from .event import Event from .channel_map import ChannelMap from typing import Generator + class EditList: """ - Represents an entire edit decision list as returned by :func:`~pycmx.parse_cmx3600()`. + Represents an entire edit decision list as returned by + :func:`~pycmx.parse_cmx3600()`. """ + def __init__(self, statements): self.title_statement = statements[0] self.event_statements = statements[1:] @@ -23,7 +27,8 @@ 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) + first_event = next( + (s for s in self.event_statements if type(s) is StmtEvent), None) if first_event: if first_event.format == 8: @@ -36,7 +41,6 @@ class EditList: return 'unknown' else: return 'unknown' - @property def channels(self) -> ChannelMap: @@ -50,7 +54,6 @@ class EditList: retval = retval | edit.channels return retval - @property def title(self) -> str: @@ -59,27 +62,23 @@ class EditList: """ return self.title_statement.title - @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 list. """ for s in self.event_statements: if type(s) is StmtUnrecognized: yield s - @property def events(self) -> Generator[Event, None, None]: 'A generator for all the events in the edit list' - is_drop = None current_event_num = None event_statements = [] for stmt in self.event_statements: - if type(stmt) is StmtFCM: - is_drop = stmt.drop - elif type(stmt) is StmtEvent: + if type(stmt) is StmtEvent: if current_event_num is None: current_event_num = stmt.event event_statements.append(stmt) @@ -103,9 +102,7 @@ class EditList: """ A generator for all of the sources in the list """ - + for stmt in self.event_statements: if type(stmt) is StmtSourceUMID: yield stmt - - diff --git a/pycmx/event.py b/pycmx/event.py index 3ff0df7..0b94236 100644 --- a/pycmx/event.py +++ b/pycmx/event.py @@ -1,19 +1,22 @@ # pycmx # (c) 2023 Jamie Hardt -from .parse_cmx_statements import (StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtEffectsName) -from .edit import Edit +from .parse_cmx_statements import ( + StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, + StmtEffectsName) +from .edit import Edit from typing import List, Generator, Optional, Tuple, Any + class Event: """ - Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same event number. - """ + Represents a collection of :class:`~pycmx.edit.Edit` s, all with the same + event number. """ def __init__(self, statements): self.statements = statements - + @property def number(self) -> int: """ @@ -28,10 +31,10 @@ class Event: will have multiple edits when a dissolve, wipe or key transition needs to be performed. """ - edits_audio = list( self._statements_with_audio_ext() ) - clip_names = self._clip_name_statements() - source_files= self._source_file_statements() - + edits_audio = list(self._statements_with_audio_ext()) + clip_names = self._clip_name_statements() + source_files = self._source_file_statements() + the_zip: List[List[Any]] = [edits_audio] if len(edits_audio) == 2: @@ -45,19 +48,19 @@ class Event: end_name = clip_name the_zip.append([start_name, end_name]) - else: + else: if len(edits_audio) == len(clip_names): the_zip.append(clip_names) else: - the_zip.append([None] * len(edits_audio) ) + the_zip.append([None] * len(edits_audio)) if len(edits_audio) == len(source_files): the_zip.append(source_files) elif len(source_files) == 1: - the_zip.append( source_files * len(edits_audio) ) + the_zip.append(source_files * len(edits_audio)) else: - the_zip.append([None] * len(edits_audio) ) - + the_zip.append([None] * len(edits_audio)) + # attach trans name to last event try: trans_statement = self._trans_name_statements()[0] @@ -65,23 +68,25 @@ class Event: trans_names.append(trans_statement) the_zip.append(trans_names) except IndexError: - the_zip.append([None] * len(edits_audio) ) + 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) + 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) 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. """ for s in self.statements: if type(s) is StmtUnrecognized: yield s - + def _trans_name_statements(self) -> List[StmtEffectsName]: return [s for s in self.statements if type(s) is StmtEffectsName] @@ -90,15 +95,14 @@ class Event: def _clip_name_statements(self) -> List[StmtClipName]: return [s for s in self.statements if type(s) is StmtClipName] - + 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]: for (s1, s2) in zip(self.statements, self.statements[1:]): if type(s1) is StmtEvent and type(s2) is StmtAudioExt: - yield (s1,s2) + yield (s1, s2) elif type(s1) is StmtEvent: yield (s1, None) - - diff --git a/pycmx/parse_cmx_events.py b/pycmx/parse_cmx_events.py index 7fe905b..29918e9 100644 --- a/pycmx/parse_cmx_events.py +++ b/pycmx/parse_cmx_events.py @@ -6,7 +6,8 @@ from .parse_cmx_statements import (parse_cmx3600_statements) from .edit_list import EditList -from typing import TextIO +from typing import TextIO + def parse_cmx3600(f: TextIO) -> EditList: """ @@ -17,4 +18,3 @@ def parse_cmx3600(f: TextIO) -> EditList: """ statements = parse_cmx3600_statements(f) return EditList(statements) - diff --git a/pycmx/parse_cmx_statements.py b/pycmx/parse_cmx_statements.py index ddd5757..3f9c414 100644 --- a/pycmx/parse_cmx_statements.py +++ b/pycmx/parse_cmx_statements.py @@ -56,44 +56,43 @@ def _parse_cmx3600_line(line: str, line_number: int) -> object: Parses a single CMX EDL line. :param line: A single EDL line. - :param line_number: The index of this line in the file. + :param line_number: The index of this line in the file. """ long_event_num_p = re.compile("^[0-9]{6} ") short_event_num_p = re.compile("^[0-9]{3} ") x_event_form_p = re.compile("^([0-9]{4,5}) ") - if isinstance(line, str): - if line.startswith("TITLE:"): - return _parse_title(line, line_number) - elif line.startswith("FCM:"): - return _parse_fcm(line, line_number) - elif long_event_num_p.match(line) != None: - length_file_128 = sum(_edl_column_widths(6, 128)) - if len(line) < length_file_128: - return _parse_long_standard_form(line, 32, line_number) - else: - return _parse_long_standard_form(line, 128, line_number) - elif short_event_num_p.match(line) != None: - return _parse_standard_form(line, line_number) - elif (m := x_event_form_p.match(line)) != None: - assert m is not None - event_field_length = len(m[1]) - return _parse_columns_for_standard_form(line, event_field_length, - 8, line_number) - elif line.startswith("AUD"): - return _parse_extended_audio_channels(line, line_number) - elif line.startswith("*"): - return _parse_remark(line[1:].strip(), line_number) - elif line.startswith(">>> SOURCE"): - return _parse_source_umid_statement(line, line_number) - elif line.startswith("EFFECTS NAME IS"): - return _parse_effects_name(line, line_number) - elif line.startswith("SPLIT:"): - return _parse_split(line, line_number) - elif line.startswith("M2"): - return _parse_motion_memory(line, line_number) + if line.startswith("TITLE:"): + return _parse_title(line, line_number) + elif line.startswith("FCM:"): + return _parse_fcm(line, line_number) + elif long_event_num_p.match(line) is not None: + length_file_128 = sum(_edl_column_widths(6, 128)) + if len(line) < length_file_128: + return _parse_long_standard_form(line, 32, line_number) else: - return _parse_unrecognized(line, line_number) + return _parse_long_standard_form(line, 128, line_number) + elif (m := x_event_form_p.match(line)) is not None: + assert m is not None + event_field_length = len(m[1]) + return _parse_columns_for_standard_form(line, event_field_length, + 8, line_number) + elif short_event_num_p.match(line) is not None: + return _parse_standard_form(line, line_number) + elif line.startswith("AUD"): + return _parse_extended_audio_channels(line, line_number) + elif line.startswith("*"): + return _parse_remark(line[1:].strip(), line_number) + elif line.startswith(">>> SOURCE"): + return _parse_source_umid_statement(line, line_number) + elif line.startswith("EFFECTS NAME IS"): + return _parse_effects_name(line, line_number) + elif line.startswith("SPLIT:"): + return _parse_split(line, line_number) + elif line.startswith("M2"): + return _parse_motion_memory(line, line_number) + else: + return _parse_unrecognized(line, line_number) def _parse_title(line, line_num) -> StmtTitle: @@ -190,5 +189,5 @@ def _parse_columns_for_standard_form(line, event_field_length, def _parse_source_umid_statement(line, line_number): - trimmed = line[3:].strip() + # trimmed = line[3:].strip() return StmtSourceUMID(name=None, umid=None, line_number=line_number) diff --git a/pycmx/transition.py b/pycmx/transition.py index 5d2ebe3..ec7fc63 100644 --- a/pycmx/transition.py +++ b/pycmx/transition.py @@ -3,11 +3,12 @@ from typing import Optional + class Transition: """ A CMX transition: a wipe, dissolve or cut. """ - + Cut = "C" Dissolve = "D" Wipe = "W" @@ -41,7 +42,7 @@ 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: @@ -56,7 +57,7 @@ class Transition: @property def effect_duration(self) -> int: """The duration of this transition, in frames of the record target. - + In the event of a key event, this is the duration of the fade in. """ return int(self.operand) @@ -86,5 +87,3 @@ class Transition: the key foreground and replaced with the key background. """ return self.transition == Transition.KeyOut - - diff --git a/pycmx/util.py b/pycmx/util.py index c2c95d4..fbaff41 100644 --- a/pycmx/util.py +++ b/pycmx/util.py @@ -3,29 +3,28 @@ # Utility functions -def collimate(a_string, column_widths): +def collimate(a_string, column_widths): """ - Split a list-type thing, like a string, into slices that are column_widths + Split a list-type thing, like a string, into slices that are column_widths length. - + >>> collimate("a b1 c2345",[2,3,3,2]) ['a ','b1 ','c23','45'] Args: a_string: The string to split. This parameter can actually be anything sliceable. - column_widths: A list of integers, each one is the length of a column. + column_widths: A list of integers, each one is the length of a column. Returns: - A list of slices. The len() of the returned list will *always* equal - len(:column_widths:). + A list of slices. The len() of the returned list will *always* equal + len(:column_widths:). """ - + if len(column_widths) == 0: return [] - + width = column_widths[0] element = a_string[:width] rest = a_string[width:] - return [element] + collimate(rest, column_widths[1:]) - + return [element] + collimate(rest, column_widths[1:]) diff --git a/tests/test_parse.py b/tests/test_parse.py index 1a6800c..8d75512 100644 --- a/tests/test_parse.py +++ b/tests/test_parse.py @@ -2,117 +2,121 @@ from unittest import TestCase 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 ] + 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)) + actual = len(list(edl.events)) + 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 ) - + self.assertTrue(type(edl.title) is str) + self.assertTrue(len(edl.title) > 0) 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 ) - self.assertTrue( event.number == index + 1 ) - - + self.assertTrue(len(event.edits) > 0) + self.assertTrue(event.number == index + 1) 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 ) + 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_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.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_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) 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) - self.assertFalse( events[0].edits[0].channels.a1) - self.assertTrue( events[0].edits[0].channels.a2) - self.assertTrue( events[2].edits[0].channels.get_audio_channel(7) ) - self.assertTrue( events[2].edits[0].channels.audio) - + events = list(edl.events) + self.assertFalse(events[0].edits[0].channels.video) + self.assertFalse(events[0].edits[0].channels.a1) + self.assertTrue(events[0].edits[0].channels.a2) + self.assertTrue(events[2].edits[0].channels.get_audio_channel(7)) + 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 ) + events = list(edl.events) - self.assertEqual( events[42].number , 43) - self.assertEqual( len(events[42].edits), 2) + self.assertEqual(events[42].number, 43) + 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].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.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].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.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].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) + 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].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) def test_line_numbers(self): with open("tests/edls/ToD_R4_LOCK3.1_030618_Video.edl") as f: edl = pycmx.parse_cmx3600(f) - events = list( edl.events ) - self.assertEqual( events[0].edits[0].line_number, 2) - self.assertEqual( events[14].edits[0].line_number, 45) - self.assertEqual( events[180].edits[0].line_number, 544) + events = list(edl.events) + self.assertEqual(events[0].edits[0].line_number, 2) + self.assertEqual(events[14].edits[0].line_number, 45) + self.assertEqual(events[180].edits[0].line_number, 544) def test_transition_name(self): - with open("tests/edls/test_25.edl","r") as f: + 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) self.assertEqual(len(events), 2839) - - # add test for edit_list.channels -