mirror of
https://github.com/iluvcapra/pycmx.git
synced 2025-12-31 08:50:54 +00:00
Doc, file path
Documentation, cleaned up interface, and we now parse file handles, not file paths
This commit is contained in:
@@ -1,4 +1,14 @@
|
|||||||
# pycmx init
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
pycmx is a module for parsing CMX 3600-style EDLs. For more information and
|
||||||
|
examples see README.md
|
||||||
|
|
||||||
|
This module (c) 2018 Jamie Hardt. For more information on your rights to
|
||||||
|
copy and reuse this software, refer to the LICENSE file included with the
|
||||||
|
distribution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from .parse_cmx_events import parse_cmx3600, Transition, Event, Edit
|
from .parse_cmx_events import parse_cmx3600, Transition, Event, Edit
|
||||||
from . import parse_cmx_events
|
from . import parse_cmx_events
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class ChannelMap:
|
|||||||
Represents a set of all the channels to which an event applies.
|
Represents a set of all the channels to which an event applies.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
chan_map = { "V" : (True, False, False),
|
_chan_map = { "V" : (True, False, False),
|
||||||
"A" : (False, True, False),
|
"A" : (False, True, False),
|
||||||
"A2" : (False, False, True),
|
"A2" : (False, False, True),
|
||||||
"AA" : (False, True, True),
|
"AA" : (False, True, True),
|
||||||
@@ -35,6 +35,7 @@ class ChannelMap:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def a1(self):
|
def a1(self):
|
||||||
|
"""True if A1 is included."""
|
||||||
return self.get_audio_channel(1)
|
return self.get_audio_channel(1)
|
||||||
|
|
||||||
@a1.setter
|
@a1.setter
|
||||||
@@ -43,6 +44,7 @@ class ChannelMap:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def a2(self):
|
def a2(self):
|
||||||
|
"""True if A2 is included."""
|
||||||
return self.get_audio_channel(2)
|
return self.get_audio_channel(2)
|
||||||
|
|
||||||
@a2.setter
|
@a2.setter
|
||||||
@@ -51,6 +53,7 @@ class ChannelMap:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def a3(self):
|
def a3(self):
|
||||||
|
"""True if A3 is included."""
|
||||||
return self.get_audio_channel(3)
|
return self.get_audio_channel(3)
|
||||||
|
|
||||||
@a3.setter
|
@a3.setter
|
||||||
@@ -59,6 +62,7 @@ class ChannelMap:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def a4(self):
|
def a4(self):
|
||||||
|
"""True if A4 is included."""
|
||||||
return self.get_audio_channel(4)
|
return self.get_audio_channel(4)
|
||||||
|
|
||||||
@a4.setter
|
@a4.setter
|
||||||
@@ -66,18 +70,20 @@ class ChannelMap:
|
|||||||
self.set_audio_channel(4,val)
|
self.set_audio_channel(4,val)
|
||||||
|
|
||||||
def get_audio_channel(self,chan_num):
|
def get_audio_channel(self,chan_num):
|
||||||
|
"""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):
|
def set_audio_channel(self,chan_num,enabled):
|
||||||
|
"""If enabled is true, chan_num will be included."""
|
||||||
if enabled:
|
if enabled:
|
||||||
self._audio_channel_set.add(chan_num)
|
self._audio_channel_set.add(chan_num)
|
||||||
elif self.get_audio_channel(chan_num):
|
elif self.get_audio_channel(chan_num):
|
||||||
self._audio_channel_set.remove(chan_num)
|
self._audio_channel_set.remove(chan_num)
|
||||||
|
|
||||||
def append_event(self, event_str):
|
def _append_event(self, event_str):
|
||||||
alt_channel_re = compile('^A(\d+)')
|
alt_channel_re = compile('^A(\d+)')
|
||||||
if event_str in self.chan_map:
|
if event_str in self._chan_map:
|
||||||
channels = self.chan_map[event_str]
|
channels = self._chan_map[event_str]
|
||||||
self.v = channels[0]
|
self.v = channels[0]
|
||||||
self.a1 = channels[1]
|
self.a1 = channels[1]
|
||||||
self.a2 = channels[2]
|
self.a2 = channels[2]
|
||||||
@@ -86,7 +92,7 @@ class ChannelMap:
|
|||||||
if matchresult:
|
if matchresult:
|
||||||
self.set_audio_channel(int( matchresult.group(1)), True )
|
self.set_audio_channel(int( matchresult.group(1)), True )
|
||||||
|
|
||||||
def append_sxt(self, audio_ext):
|
def _append_sxt(self, audio_ext):
|
||||||
self.a3 = ext.audio3
|
self.a3 = ext.audio3
|
||||||
self.a4 = ext.audio4
|
self.a4 = ext.audio4
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,35 @@ from .channel_map import ChannelMap
|
|||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
def parse_cmx3600(path):
|
def parse_cmx3600(f):
|
||||||
statements = parse_cmx3600_statements(path)
|
"""
|
||||||
|
Parse a CMX 3600 EDL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
f : a file-like object, anything that's readlines-able.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An :obj:`EditList`.
|
||||||
|
"""
|
||||||
|
statements = parse_cmx3600_statements(f)
|
||||||
return EditList(statements)
|
return EditList(statements)
|
||||||
|
|
||||||
|
|
||||||
class EditList:
|
class EditList:
|
||||||
|
"""
|
||||||
|
Represents an entire edit decision list as returned by `parse_cmx3600()`.
|
||||||
|
|
||||||
|
"""
|
||||||
def __init__(self, statements):
|
def __init__(self, statements):
|
||||||
self.title_statement = statements[0]
|
self.title_statement = statements[0]
|
||||||
self.event_statements = statements[1:]
|
self.event_statements = statements[1:]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
|
"""
|
||||||
|
The title of this edit list, as attensted by the 'TITLE:' statement on
|
||||||
|
the first line.
|
||||||
|
"""
|
||||||
'The title of the edit list'
|
'The title of the edit list'
|
||||||
return self.title_statement.title
|
return self.title_statement.title
|
||||||
|
|
||||||
@@ -59,44 +76,81 @@ class Edit:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def channels(self):
|
def channels(self):
|
||||||
|
"""
|
||||||
|
Get the :obj:`ChannelMap` object associated with this Edit.
|
||||||
|
"""
|
||||||
cm = ChannelMap()
|
cm = ChannelMap()
|
||||||
cm.append_event(self.edit_statement.channels)
|
cm._append_event(self.edit_statement.channels)
|
||||||
if self.audio_ext != None:
|
if self.audio_ext != None:
|
||||||
cm.append_ext(self.audio_ext)
|
cm._append_ext(self.audio_ext)
|
||||||
return cm
|
return cm
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def transition(self):
|
def transition(self):
|
||||||
|
"""
|
||||||
|
Get the :obj:`Transition` object associated with this edit.
|
||||||
|
"""
|
||||||
return Transition(self.edit_statement.trans, self.edit_statement.trans_op)
|
return Transition(self.edit_statement.trans, self.edit_statement.trans_op)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_in(self):
|
def source_in(self):
|
||||||
|
"""
|
||||||
|
Get the source in timecode.
|
||||||
|
"""
|
||||||
return self.edit_statement.source_in
|
return self.edit_statement.source_in
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_out(self):
|
def source_out(self):
|
||||||
|
"""
|
||||||
|
Get the source out timecode.
|
||||||
|
"""
|
||||||
|
|
||||||
return self.edit_statement.source_out
|
return self.edit_statement.source_out
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def record_in(self):
|
def record_in(self):
|
||||||
|
"""
|
||||||
|
Get the record in timecode.
|
||||||
|
"""
|
||||||
|
|
||||||
return self.edit_statement.record_in
|
return self.edit_statement.record_in
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def record_out(self):
|
def record_out(self):
|
||||||
|
"""
|
||||||
|
Get the record out timecode.
|
||||||
|
"""
|
||||||
|
|
||||||
return self.edit_statement.record_out
|
return self.edit_statement.record_out
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self):
|
def source(self):
|
||||||
|
"""
|
||||||
|
Get the source column. This is the 8, 32 or 128-character string on the
|
||||||
|
event record line, this usually references the tape name of the source.
|
||||||
|
"""
|
||||||
return self.edit_statement.source
|
return self.edit_statement.source
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_file(self):
|
def source_file(self):
|
||||||
return self.source_file_statement.filename
|
"""
|
||||||
|
Get the source file, as attested by a "* SOURCE FILE" remark on the
|
||||||
|
EDL. This will return None if the information is not present.
|
||||||
|
"""
|
||||||
|
if self.source_file_statement is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return self.source_file_statement.filename
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def clip_name(self):
|
def clip_name(self):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
if self.clip_name_statement != None:
|
if self.clip_name_statement != None:
|
||||||
return self.clip_name_statement.name
|
return self.clip_name_statement.name
|
||||||
else:
|
else:
|
||||||
@@ -110,10 +164,16 @@ class Event:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def number(self):
|
def number(self):
|
||||||
return self._edit_statements()[0].event
|
"""Return the event number."""
|
||||||
|
return int(self._edit_statements()[0].event)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def edits(self):
|
def edits(self):
|
||||||
|
"""
|
||||||
|
Returns the edits. Most events will have a single edit, a single event
|
||||||
|
will have multiple edits when a dissolve, wipe or key transition needs
|
||||||
|
to be performed.
|
||||||
|
"""
|
||||||
edits_audio = list( self._statements_with_audio_ext() )
|
edits_audio = list( self._statements_with_audio_ext() )
|
||||||
clip_names = self._clip_name_statements()
|
clip_names = self._clip_name_statements()
|
||||||
source_files= self._source_file_statements()
|
source_files= self._source_file_statements()
|
||||||
@@ -170,12 +230,18 @@ class Transition:
|
|||||||
"""Represents a CMX transition, a wipe, dissolve or cut."""
|
"""Represents a CMX transition, a wipe, dissolve or cut."""
|
||||||
|
|
||||||
Cut = "C"
|
Cut = "C"
|
||||||
|
|
||||||
Dissolve = "D"
|
Dissolve = "D"
|
||||||
|
|
||||||
Wipe = "W"
|
Wipe = "W"
|
||||||
|
|
||||||
KeyBackground = "KB"
|
KeyBackground = "KB"
|
||||||
|
|
||||||
Key = "K"
|
Key = "K"
|
||||||
|
|
||||||
KeyOut = "KO"
|
KeyOut = "KO"
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, transition, operand):
|
def __init__(self, transition, operand):
|
||||||
self.transition = transition
|
self.transition = transition
|
||||||
self.operand = operand
|
self.operand = operand
|
||||||
@@ -184,6 +250,9 @@ class Transition:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def kind(self):
|
def kind(self):
|
||||||
|
"""
|
||||||
|
Return the kind of transition: Cut, Wipe, etc
|
||||||
|
"""
|
||||||
if self.cut:
|
if self.cut:
|
||||||
return Transition.Cut
|
return Transition.Cut
|
||||||
elif self.dissolve:
|
elif self.dissolve:
|
||||||
@@ -216,7 +285,7 @@ class Transition:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def effect_duration(self):
|
def effect_duration(self):
|
||||||
""""`The duration of this transition, in frames of the record target.
|
"""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.
|
In the event of a key event, this is the duration of the fade in.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs mor
|
|||||||
StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
|
StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
|
||||||
|
|
||||||
|
|
||||||
def parse_cmx3600_statements(path):
|
def parse_cmx3600_statements(file):
|
||||||
with open(path,'r') as file:
|
"""
|
||||||
lines = file.readlines()
|
Return a list of every statement in the file argument.
|
||||||
line_numbers = count()
|
"""
|
||||||
return [parse_cmx3600_line(line.strip(), line_number) \
|
lines = file.readlines()
|
||||||
for (line, line_number) in zip(lines,line_numbers)]
|
line_numbers = count()
|
||||||
|
return [_parse_cmx3600_line(line.strip(), line_number) \
|
||||||
|
for (line, line_number) in zip(lines,line_numbers)]
|
||||||
|
|
||||||
def edl_column_widths(event_field_length, source_field_length):
|
def _edl_column_widths(event_field_length, source_field_length):
|
||||||
return [event_field_length,2, source_field_length,1,
|
return [event_field_length,2, source_field_length,1,
|
||||||
4,2, # chans
|
4,2, # chans
|
||||||
4,1, # trans
|
4,1, # trans
|
||||||
@@ -40,63 +42,63 @@ def edl_column_widths(event_field_length, source_field_length):
|
|||||||
11,1,
|
11,1,
|
||||||
11]
|
11]
|
||||||
|
|
||||||
def edl_m2_column_widths():
|
def _edl_m2_column_widths():
|
||||||
return [2, # "M2"
|
return [2, # "M2"
|
||||||
3,3, #
|
3,3, #
|
||||||
8,8,1,4,2,1,4,13,3,1,1]
|
8,8,1,4,2,1,4,13,3,1,1]
|
||||||
|
|
||||||
|
|
||||||
def parse_cmx3600_line(line, line_number):
|
def _parse_cmx3600_line(line, line_number):
|
||||||
long_event_num_p = re.compile("^[0-9]{6} ")
|
long_event_num_p = re.compile("^[0-9]{6} ")
|
||||||
short_event_num_p = re.compile("^[0-9]{3} ")
|
short_event_num_p = re.compile("^[0-9]{3} ")
|
||||||
|
|
||||||
if isinstance(line,str):
|
if isinstance(line,str):
|
||||||
if line.startswith("TITLE:"):
|
if line.startswith("TITLE:"):
|
||||||
return parse_title(line,line_number)
|
return _parse_title(line,line_number)
|
||||||
elif line.startswith("FCM:"):
|
elif line.startswith("FCM:"):
|
||||||
return parse_fcm(line, line_number)
|
return _parse_fcm(line, line_number)
|
||||||
elif long_event_num_p.match(line) != None:
|
elif long_event_num_p.match(line) != None:
|
||||||
length_file_128 = sum(edl_column_widths(6,128))
|
length_file_128 = sum(_edl_column_widths(6,128))
|
||||||
if len(line) < length_file_128:
|
if len(line) < length_file_128:
|
||||||
return parse_long_standard_form(line, 32, line_number)
|
return _parse_long_standard_form(line, 32, line_number)
|
||||||
else:
|
else:
|
||||||
return parse_long_standard_form(line, 128, line_number)
|
return _parse_long_standard_form(line, 128, line_number)
|
||||||
elif short_event_num_p.match(line) != None:
|
elif short_event_num_p.match(line) != None:
|
||||||
return parse_standard_form(line, line_number)
|
return _parse_standard_form(line, line_number)
|
||||||
elif line.startswith("AUD"):
|
elif line.startswith("AUD"):
|
||||||
return parse_extended_audio_channels(line,line_number)
|
return _parse_extended_audio_channels(line,line_number)
|
||||||
elif line.startswith("*"):
|
elif line.startswith("*"):
|
||||||
return parse_remark( line[1:].strip(), line_number)
|
return _parse_remark( line[1:].strip(), line_number)
|
||||||
elif line.startswith(">>>"):
|
elif line.startswith(">>>"):
|
||||||
return parse_trailer_statement(line, line_number)
|
return _parse_trailer_statement(line, line_number)
|
||||||
elif line.startswith("EFFECTS NAME IS"):
|
elif line.startswith("EFFECTS NAME IS"):
|
||||||
return parse_effects_name(line, line_number)
|
return _parse_effects_name(line, line_number)
|
||||||
elif line.startswith("SPLIT:"):
|
elif line.startswith("SPLIT:"):
|
||||||
return parse_split(line, line_number)
|
return _parse_split(line, line_number)
|
||||||
elif line.startswith("M2"):
|
elif line.startswith("M2"):
|
||||||
return parse_motion_memory(line, line_number)
|
return _parse_motion_memory(line, line_number)
|
||||||
else:
|
else:
|
||||||
return parse_unrecognized(line, line_number)
|
return _parse_unrecognized(line, line_number)
|
||||||
|
|
||||||
|
|
||||||
def parse_title(line, line_num):
|
def _parse_title(line, line_num):
|
||||||
title = line[6:].strip()
|
title = line[6:].strip()
|
||||||
return StmtTitle(title=title,line_number=line_num)
|
return StmtTitle(title=title,line_number=line_num)
|
||||||
|
|
||||||
def parse_fcm(line, line_num):
|
def _parse_fcm(line, line_num):
|
||||||
val = line[4:].strip()
|
val = line[4:].strip()
|
||||||
if val == "DROP FRAME":
|
if val == "DROP FRAME":
|
||||||
return StmtFCM(drop= True, line_number=line_num)
|
return StmtFCM(drop= True, line_number=line_num)
|
||||||
else:
|
else:
|
||||||
return StmtFCM(drop= False, line_number=line_num)
|
return StmtFCM(drop= False, line_number=line_num)
|
||||||
|
|
||||||
def parse_long_standard_form(line,source_field_length, line_number):
|
def _parse_long_standard_form(line,source_field_length, line_number):
|
||||||
return parse_columns_for_standard_form(line, 6, source_field_length, line_number)
|
return _parse_columns_for_standard_form(line, 6, source_field_length, line_number)
|
||||||
|
|
||||||
def parse_standard_form(line, line_number):
|
def _parse_standard_form(line, line_number):
|
||||||
return parse_columns_for_standard_form(line, 3, 8, line_number)
|
return _parse_columns_for_standard_form(line, 3, 8, line_number)
|
||||||
|
|
||||||
def parse_extended_audio_channels(line, line_number):
|
def _parse_extended_audio_channels(line, line_number):
|
||||||
content = line.strip()
|
content = line.strip()
|
||||||
if content == "AUD 3":
|
if content == "AUD 3":
|
||||||
return StmtAudioExt(audio3=True, audio4=False, line_number=line_number)
|
return StmtAudioExt(audio3=True, audio4=False, line_number=line_number)
|
||||||
@@ -107,7 +109,7 @@ def parse_extended_audio_channels(line, line_number):
|
|||||||
else:
|
else:
|
||||||
return StmtUnrecognized(content=line, line_number=line_number)
|
return StmtUnrecognized(content=line, line_number=line_number)
|
||||||
|
|
||||||
def parse_remark(line, line_number):
|
def _parse_remark(line, line_number):
|
||||||
if line.startswith("FROM CLIP NAME:"):
|
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:"):
|
elif line.startswith("TO CLIP NAME:"):
|
||||||
@@ -117,11 +119,11 @@ def parse_remark(line, line_number):
|
|||||||
else:
|
else:
|
||||||
return StmtRemark(text=line, line_number=line_number)
|
return StmtRemark(text=line, line_number=line_number)
|
||||||
|
|
||||||
def parse_effects_name(line, line_number):
|
def _parse_effects_name(line, line_number):
|
||||||
name = line[16:].strip()
|
name = line[16:].strip()
|
||||||
return StmtEffectsName(name=name, line_number=line_number)
|
return StmtEffectsName(name=name, line_number=line_number)
|
||||||
|
|
||||||
def parse_split(line, line_number):
|
def _parse_split(line, line_number):
|
||||||
split_type = line[10:21]
|
split_type = line[10:21]
|
||||||
is_video = False
|
is_video = False
|
||||||
if split_type.startswith("VIDEO"):
|
if split_type.startswith("VIDEO"):
|
||||||
@@ -131,15 +133,15 @@ def parse_split(line, line_number):
|
|||||||
return StmtSplitEdit(video=is_video, magnitude=split_mag, line_number=line_number)
|
return StmtSplitEdit(video=is_video, magnitude=split_mag, line_number=line_number)
|
||||||
|
|
||||||
|
|
||||||
def parse_motion_memory(line, line_number):
|
def _parse_motion_memory(line, line_number):
|
||||||
return StmtMotionMemory(source = "", fps="")
|
return StmtMotionMemory(source = "", fps="")
|
||||||
|
|
||||||
|
|
||||||
def parse_unrecognized(line, line_number):
|
def _parse_unrecognized(line, line_number):
|
||||||
return StmtUnrecognized(content=line, line_number=line_number)
|
return StmtUnrecognized(content=line, line_number=line_number)
|
||||||
|
|
||||||
def parse_columns_for_standard_form(line, event_field_length, source_field_length, line_number):
|
def _parse_columns_for_standard_form(line, event_field_length, source_field_length, line_number):
|
||||||
col_widths = edl_column_widths(event_field_length, source_field_length)
|
col_widths = _edl_column_widths(event_field_length, source_field_length)
|
||||||
|
|
||||||
if sum(col_widths) > len(line):
|
if sum(col_widths) > len(line):
|
||||||
return StmtUnrecognized(content=line, line_number=line_number)
|
return StmtUnrecognized(content=line, line_number=line_number)
|
||||||
@@ -158,7 +160,7 @@ def parse_columns_for_standard_form(line, event_field_length, source_field_lengt
|
|||||||
line_number=line_number)
|
line_number=line_number)
|
||||||
|
|
||||||
|
|
||||||
def parse_trailer_statement(line, line_number):
|
def _parse_trailer_statement(line, line_number):
|
||||||
trimmed = line[3:].strip()
|
trimmed = line[3:].strip()
|
||||||
return StmtTrailer(trimmed, line_number=line_number)
|
return StmtTrailer(trimmed, line_number=line_number)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,22 @@
|
|||||||
# Utility functions
|
# Utility functions
|
||||||
|
|
||||||
def collimate(a_string, column_widths):
|
def collimate(a_string, column_widths):
|
||||||
'Splits a string into substrings that are column_widths length.'
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of slices. The len() of the returned list will *always* equal
|
||||||
|
len(:column_widths:).
|
||||||
|
"""
|
||||||
|
|
||||||
if len(column_widths) == 0:
|
if len(column_widths) == 0:
|
||||||
return []
|
return []
|
||||||
@@ -14,51 +29,3 @@ def collimate(a_string, column_widths):
|
|||||||
rest = a_string[width:]
|
rest = a_string[width:]
|
||||||
return [element] + collimate(rest, column_widths[1:])
|
return [element] + collimate(rest, column_widths[1:])
|
||||||
|
|
||||||
|
|
||||||
class NamedTupleParser:
|
|
||||||
"""
|
|
||||||
Accepts a list of namedtuple and the client can step through the list with
|
|
||||||
parser operations such as `accept()` and `expect()`
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tuple_list):
|
|
||||||
self.tokens = tuple_list
|
|
||||||
self.current_token = None
|
|
||||||
|
|
||||||
def peek(self):
|
|
||||||
"""
|
|
||||||
Returns the token to come after the `current_token` without
|
|
||||||
popping the current token.
|
|
||||||
"""
|
|
||||||
return self.tokens[0]
|
|
||||||
|
|
||||||
def at_end(self):
|
|
||||||
"`True` if the `current_token` is the last one."
|
|
||||||
return len(self.tokens) == 0
|
|
||||||
|
|
||||||
def next_token(self):
|
|
||||||
"Sets `current_token` to the next token popped from the list"
|
|
||||||
self.current_token = self.peek()
|
|
||||||
self.tokens = self.tokens[1:]
|
|
||||||
|
|
||||||
def accept(self, type_name):
|
|
||||||
"""
|
|
||||||
If the next token.__name__ is `type_name`, returns true and advances
|
|
||||||
to the next token with `next_token()`.
|
|
||||||
"""
|
|
||||||
if self.at_end():
|
|
||||||
return False
|
|
||||||
elif (type(self.peek()).__name__ == type_name ):
|
|
||||||
self.next_token()
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def expect(self, type_name):
|
|
||||||
"""
|
|
||||||
If the next token.__name__ is `type_name`, the parser is advanced.
|
|
||||||
If it is not, an assertion failure occurs.
|
|
||||||
"""
|
|
||||||
assert( self.accept(type_name) )
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,61 +14,61 @@ class TestParse(TestCase):
|
|||||||
|
|
||||||
counts = [ 287, 466, 250 , 376, 120 ]
|
counts = [ 287, 466, 250 , 376, 120 ]
|
||||||
|
|
||||||
|
|
||||||
for fn, count in zip(files, counts):
|
for fn, count in zip(files, counts):
|
||||||
edl = pycmx.parse_cmx3600(f"tests/edls/{fn}" )
|
with open(f"tests/edls/{fn}" ,'r') as f:
|
||||||
actual = len(list(edl.events ))
|
edl = pycmx.parse_cmx3600(f)
|
||||||
self.assertTrue( actual == count , f"expected {count} in file {fn} but found {actual}")
|
actual = len(list(edl.events ))
|
||||||
|
self.assertTrue( actual == count , f"expected {count} in file {fn} but found {actual}")
|
||||||
|
|
||||||
|
|
||||||
def test_events(self):
|
def test_events(self):
|
||||||
edl = pycmx.parse_cmx3600("tests/edls/TEST.edl")
|
with open("tests/edls/TEST.edl",'r') as f:
|
||||||
events = list( edl.events )
|
edl = pycmx.parse_cmx3600(f)
|
||||||
|
events = list( edl.events )
|
||||||
self.assertEqual( int(events[0].number) , 1)
|
|
||||||
self.assertEqual( events[0].edits[0].source , "OY_HEAD_")
|
self.assertEqual( int(events[0].number) , 1)
|
||||||
self.assertEqual( events[0].edits[0].clip_name , "HEAD LEADER MONO")
|
self.assertEqual( events[0].edits[0].source , "OY_HEAD_")
|
||||||
self.assertEqual( events[0].edits[0].source_file , "OY_HEAD_LEADER.MOV")
|
self.assertEqual( events[0].edits[0].clip_name , "HEAD LEADER MONO")
|
||||||
self.assertEqual( events[0].edits[0].source_in , "00:00:00:00")
|
self.assertEqual( events[0].edits[0].source_file , "OY_HEAD_LEADER.MOV")
|
||||||
self.assertEqual( events[0].edits[0].source_out , "00:00:00:00")
|
self.assertEqual( events[0].edits[0].source_in , "00:00:00:00")
|
||||||
self.assertEqual( events[0].edits[0].record_in , "01:00:00:00")
|
self.assertEqual( events[0].edits[0].source_out , "00:00:00:00")
|
||||||
self.assertEqual( events[0].edits[0].record_out , "01:00:08:00")
|
self.assertEqual( events[0].edits[0].record_in , "01:00:00:00")
|
||||||
self.assertTrue( events[0].edits[0].transition.kind == pycmx.Transition.Cut)
|
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_mop(self):
|
def test_channel_mop(self):
|
||||||
edl = pycmx.parse_cmx3600("tests/edls/TEST.edl")
|
with open("tests/edls/TEST.edl",'r') as f:
|
||||||
events = list( edl.events )
|
edl = pycmx.parse_cmx3600(f)
|
||||||
self.assertFalse( events[0].edits[0].channels.video)
|
events = list( edl.events )
|
||||||
self.assertFalse( events[0].edits[0].channels.a1)
|
self.assertFalse( events[0].edits[0].channels.video)
|
||||||
self.assertTrue( events[0].edits[0].channels.a2)
|
self.assertFalse( events[0].edits[0].channels.a1)
|
||||||
self.assertTrue( events[2].edits[0].channels.get_audio_channel(7) )
|
self.assertTrue( events[0].edits[0].channels.a2)
|
||||||
|
self.assertTrue( events[2].edits[0].channels.get_audio_channel(7) )
|
||||||
|
|
||||||
|
|
||||||
def test_multi_edit_events(self):
|
def test_multi_edit_events(self):
|
||||||
edl = pycmx.parse_cmx3600("tests/edls/TEST.edl")
|
with open("tests/edls/TEST.edl",'r') as f:
|
||||||
events = list( edl.events )
|
edl = pycmx.parse_cmx3600(f)
|
||||||
|
events = list( edl.events )
|
||||||
|
|
||||||
|
self.assertEqual( int(events[42].number) , 43)
|
||||||
|
self.assertEqual( len(events[42].edits), 2)
|
||||||
|
|
||||||
self.assertEqual( int(events[42].number) , 43)
|
self.assertEqual( events[42].edits[0].source , "TC_R1_V1")
|
||||||
self.assertEqual( len(events[42].edits), 2)
|
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[0].source , "TC_R1_V1")
|
self.assertEqual( events[42].edits[1].clip_name , "TC R1 V6 TEMP2 ST FX.WAV")
|
||||||
self.assertEqual( events[42].edits[0].clip_name , "TC R1 V1.2 TEMP1 FX ST.WAV")
|
self.assertEqual( events[42].edits[1].source_in , "00:00:00:00")
|
||||||
self.assertEqual( events[42].edits[0].source_in , "00:00:00:00")
|
self.assertEqual( events[42].edits[1].source_out , "00:00:00:00")
|
||||||
self.assertEqual( events[42].edits[0].source_out , "00:00:00:00")
|
self.assertEqual( events[42].edits[1].record_in , "01:08:56:09")
|
||||||
self.assertEqual( events[42].edits[0].record_in , "01:08:56:09")
|
self.assertEqual( events[42].edits[1].record_out , "01:08:56:11")
|
||||||
self.assertEqual( events[42].edits[0].record_out , "01:08:56:09")
|
self.assertTrue( events[42].edits[1].transition.kind == pycmx.Transition.Dissolve)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user