53 Commits

Author SHA1 Message Date
Jamie Hardt
7fa22d4b85 Update setup.py
Version 1.1.2
2022-11-13 17:50:26 -08:00
Jamie Hardt
42f2de54b5 Update README.md
Removed "Platform Lifecycle" section, this work is done
2022-11-13 17:42:46 -08:00
Jamie Hardt
f7d1432014 Update pythonpublish.yml 2022-11-13 17:41:55 -08:00
Jamie Hardt
db4eadb73e Update python-package.yml
Updated actions/setup-python version pin
2022-11-13 17:37:50 -08:00
Jamie Hardt
3305bc7920 Removing Python 3.5 from support 2022-11-13 17:33:57 -08:00
Jamie Hardt
6ba77b3568 Adding newer python versions
to setup.rb and test grid
2022-11-13 17:32:30 -08:00
Jamie Hardt
c68f8bca80 Fixed spelling of an re 2022-11-13 17:29:10 -08:00
Jamie Hardt
284267c9c0 Fixed some bugs picked up in flake8 2022-11-13 17:25:56 -08:00
Jamie Hardt
bd196f2dbf Create python-package.yml 2022-11-13 17:21:54 -08:00
Jamie Hardt
b14a9a6319 Update .gitignore 2022-01-16 17:16:14 -08:00
Jamie Hardt
0cbd01f418 Update README.md 2020-01-05 14:55:43 -08:00
Jamie Hardt
50d48708e9 .idea files 2020-01-04 22:38:25 -08:00
Jamie Hardt
f67c4ac2c5 Update setup.py 2020-01-04 22:38:15 -08:00
Jamie Hardt
1b8a3c3288 Create pythonpublish.yml 2020-01-04 22:36:54 -08:00
Jamie Hardt
b37b57d7c9 Removed pypi upload code 2020-01-04 22:36:24 -08:00
Jamie Hardt
a9937683e5 Update setup.py 2020-01-03 09:43:38 -08:00
Jamie Hardt
4fae65fa8d Update .travis.yml
Adding python 3.8
2020-01-03 09:42:40 -08:00
Jamie Hardt
566e6257f4 Added 'audio' method to ChannelMap
Added `audio` property to channelmap to test if it contains any audio channels.
2019-08-18 10:57:16 -07:00
Jamie Hardt
c56d2066ad Update setup.py
v1.0.1
2019-08-17 13:06:39 -07:00
Jamie Hardt
8b49a788ae Add version 3.7 support checks 2019-08-17 13:01:51 -07:00
Jamie Hardt
b31450f03d Update README.md
codecov badge
2019-01-05 12:57:16 -08:00
Jamie Hardt
5d14c3177a Update .travis.yml 2019-01-05 12:55:09 -08:00
Jamie Hardt
08dea6031d Update .travis.yml
switch to codecov
2019-01-05 12:49:52 -08:00
Jamie Hardt
d23fa33558 Update setup.py
Added version 2.7
2019-01-04 19:06:46 -08:00
Jamie Hardt
fcc4732d1a Update .travis.yml 2019-01-04 18:14:00 -08:00
Jamie Hardt
47c1ad96f0 Update .travis.yml 2019-01-04 18:01:49 -08:00
Jamie Hardt
804f649570 Configuring Coveralls 2019-01-04 18:01:23 -08:00
Jamie Hardt
58483198c3 Update .travis.yml 2019-01-04 09:12:21 -08:00
Jamie Hardt
aa01e9ad2d Update .travis.yml 2019-01-03 22:03:23 -08:00
Jamie Hardt
464052f510 Update .travis.yml
Adding 2.7 to see what happens
2019-01-03 20:09:39 -08:00
Jamie Hardt
3ba28a61dd Update README.md
Removed "should I use this?"
2019-01-03 19:47:38 -08:00
Jamie Hardt
b80339267a Version 1.0 2019-01-03 19:40:03 -08:00
Jamie Hardt
27d1073f8c Update test_parse.py 2019-01-03 19:35:20 -08:00
Jamie Hardt
0840ade312 Update test_parse.py
One more...
2019-01-03 19:33:26 -08:00
Jamie Hardt
a52e4329ce Update test_parse.py 2019-01-03 19:31:33 -08:00
Jamie Hardt
47772b21d0 Merge branch 'master' of https://github.com/iluvcapra/pycmx 2019-01-03 19:28:50 -08:00
Jamie Hardt
7b4a76448e Changed a format string in the tests so 3.5 should build 2019-01-03 19:28:48 -08:00
Jamie Hardt
c6c5d15e09 Update README.md 2019-01-03 11:13:31 -08:00
Jamie Hardt
c439ea1fbe Update README.md 2019-01-03 11:12:52 -08:00
Jamie Hardt
68f118ab6b Update README.md
Removed Travis badge
2019-01-01 12:25:36 -08:00
Jamie Hardt
a20491297e Update README.md 2018-12-31 12:11:17 -08:00
Jamie Hardt
9d57c2d374 Update README.md 2018-12-31 12:03:37 -08:00
Jamie Hardt
1882cc5308 Can't support 3.7 yet? 2018-12-31 11:59:59 -08:00
Jamie Hardt
c57fe94335 Removed versions I don't support 2018-12-31 11:58:04 -08:00
Jamie Hardt
007661ef38 Merge branch 'master' of https://github.com/iluvcapra/pycmx 2018-12-31 11:52:23 -08:00
Jamie Hardt
f34c6dd4db Update .travis.yml
Added more versions to travis
2018-12-31 11:52:08 -08:00
Jamie Hardt
eb89708bab Update README.md
badges
2018-12-31 11:49:07 -08:00
Jamie Hardt
9fde608fa0 Update setup.py
Added version classifiers
2018-12-31 11:47:34 -08:00
Jamie Hardt
4c4ca428f2 Update README.md
Badges
2018-12-31 11:43:59 -08:00
Jamie Hardt
fe4e3b9d85 Update README.md 2018-12-31 11:35:34 -08:00
Jamie Hardt
119467a884 Update README.md
Added pypi badge
2018-12-31 11:35:11 -08:00
Jamie Hardt
b5a3285e64 Nudge version
Also saved upload command to a script
2018-12-29 16:31:48 -08:00
Jamie Hardt
af1c532a67 Some SourceUMID Impl 2018-12-29 15:16:26 -08:00
19 changed files with 227 additions and 59 deletions

40
.github/workflows/python-package.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4.3.0
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
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
- name: Test with pytest
run: |
pytest

26
.github/workflows/pythonpublish.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Upload Python Package
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_APIKEY }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@
# Vim Swapfiles # Vim Swapfiles
*.swp *.swp
.DS_Store

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/workspace.xml

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/pycmx.iml" filepath="$PROJECT_DIR$/.idea/pycmx.iml" />
</modules>
</component>
</project>

11
.idea/pycmx.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.7" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="Unittests" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -1,7 +1,18 @@
language: python language: python
python: python:
- "3.8"
- "3.7"
- "3.6" - "3.6"
- "3.5"
- "2.7"
script: script:
- "python3 setup.py test" - "python setup.py test"
- "py.test tests/ -v --cov pycmx --cov-report term-missing"
install: install:
- "pip3 install setuptools" - "pip install setuptools"
before_install:
- "pip install coverage"
- "pip install codecov"
- "pip install pytest-cov==2.5.0"
after_success:
- "codecov"

View File

@@ -1,4 +1,6 @@
[![Build Status](https://travis-ci.com/iluvcapra/pycmx.svg?branch=master)](https://travis-ci.com/iluvcapra/pycmx) [![Documentation Status](https://readthedocs.org/projects/pycmx/badge/?version=latest)](https://pycmx.readthedocs.io/en/latest/?badge=latest) [![Build Status](https://travis-ci.com/iluvcapra/pycmx.svg?branch=master)](https://travis-ci.com/iluvcapra/pycmx)
[![codecov](https://codecov.io/gh/iluvcapra/pycmx/branch/master/graph/badge.svg)](https://codecov.io/gh/iluvcapra/pycmx)
[![Documentation Status](https://readthedocs.org/projects/pycmx/badge/?version=latest)](https://pycmx.readthedocs.io/en/latest/?badge=latest) ![](https://img.shields.io/github/license/iluvcapra/pycmx.svg) ![](https://img.shields.io/pypi/pyversions/pycmx.svg) [![](https://img.shields.io/pypi/v/pycmx.svg)](https://pypi.org/project/pycmx/) ![](https://img.shields.io/pypi/wheel/pycmx.svg)
# pycmx # pycmx
@@ -82,32 +84,4 @@ Audio channel 7 is present
False False
``` ```
## How is this different from `python-edl`?
There are two important differences between `import edl` and `import pycmx`
and motivated my development of this module.
1. The `pycmx` parser doesn't take timecode or framerates into account,
and strictly treats timecodes like opaque values. As far as `pycmx` is
concerend, they're just strings. This was done because in my experience,
the frame rate of an EDL is often difficult to precisely determine and
often the frame rate of various sources is different from the frame rate
of the target track.
In any event, timecodes in an EDL are a kind of *address* and are not
exactly scalar, they're meant to point to a particular block of video or
audio data on a medium and presuming that they refer to a real time, or
duration, or are convertible, etc. isn't always safe.
2. The `pycmx` parser reads event numbers and keeps track of which EDL rows
are meant to happen "at the same time," with two decks. This makes it
easier to reconstruct transition A/B clips, and read clip names from
such events appropriately.
## Should I Use This?
At this time, this is (at best) beta software. I feel like the interface is
about where where I'd like it to be but more testing is required.
Contributions are welcome and will make this module production-ready all the
faster! Please reach out or file a ticket!

View File

@@ -8,7 +8,7 @@ copy and reuse this software, refer to the LICENSE file included with the
distribution. distribution.
""" """
__version__ = '0.8' __version__ = '1.1.1'
__author__ = 'Jamie Hardt' __author__ = 'Jamie Hardt'
from .parse_cmx_events import parse_cmx3600 from .parse_cmx_events import parse_cmx3600

View File

@@ -27,6 +27,11 @@ class ChannelMap:
'True if video is included' 'True if video is included'
return self.v return self.v
@property
def audio(self):
'True if an audio channel is included'
return len(self._audio_channel_set) > 0
@property @property
def channels(self): def channels(self):
'A generator for each audio channel' 'A generator for each audio channel'
@@ -81,7 +86,7 @@ class ChannelMap:
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(r'^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]
@@ -93,6 +98,14 @@ class ChannelMap:
self.set_audio_channel(int( matchresult.group(1)), True ) self.set_audio_channel(int( matchresult.group(1)), True )
def _append_ext(self, audio_ext): def _append_ext(self, audio_ext):
self.a3 = ext.audio3 self.a3 = audio_ext.audio3
self.a4 = ext.audio4 self.a4 = audio_ext.audio4
def __or__(self, other):
"""
Return the logical union of this channel map with another
"""
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)

View File

@@ -1,8 +1,9 @@
# pycmx # pycmx
# (c) 2018 Jamie Hardt # (c) 2018 Jamie Hardt
from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent) from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSourceUMID)
from .event import Event from .event import Event
from .channel_map import ChannelMap
class EditList: class EditList:
""" """
@@ -13,6 +14,42 @@ class EditList:
self.title_statement = statements[0] self.title_statement = statements[0]
self.event_statements = statements[1:] self.event_statements = statements[1:]
@property
def format(self):
"""
The detected format of the EDL. Possible values are: `3600`,`File32`,
`File128`, and `unknown`
"""
first_event = next( (s for s in self.event_statements if type(s) is StmtEvent), None)
if first_event:
if first_event.format == 8:
return '3600'
elif first_event.format == 32:
return 'File32'
elif first_event.format == 128:
return 'File128'
else:
return 'unknown'
else:
return 'unknown'
@property
def channels(self):
"""
Return the union of every channel channel.
"""
retval = ChannelMap()
for event in self.events:
for edit in event.edits:
retval = retval | edit.channels
return retval
@property @property
def title(self): def title(self):
""" """
@@ -54,8 +91,21 @@ class EditList:
else: else:
event_statements.append(stmt) event_statements.append(stmt)
elif type(stmt) is StmtSourceUMID:
break
else: else:
event_statements.append(stmt) event_statements.append(stmt)
yield Event(statements=event_statements) yield Event(statements=event_statements)
@property
def sources(self):
"""
A generator for all of the sources in the list
"""
for stmt in self.event_statements:
if type(stmt) is StmtSourceUMID:
yield stmt

View File

@@ -11,13 +11,13 @@ from .util import collimate
StmtTitle = namedtuple("Title",["title","line_number"]) StmtTitle = namedtuple("Title",["title","line_number"])
StmtFCM = namedtuple("FCM",["drop","line_number"]) StmtFCM = namedtuple("FCM",["drop","line_number"])
StmtEvent = namedtuple("Event",["event","source","channels","trans",\ StmtEvent = namedtuple("Event",["event","source","channels","trans",\
"trans_op","source_in","source_out","record_in","record_out","line_number"]) "trans_op","source_in","source_out","record_in","record_out","format","line_number"])
StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"]) StmtAudioExt = namedtuple("AudioExt",["audio3","audio4","line_number"])
StmtClipName = namedtuple("ClipName",["name","affect","line_number"]) StmtClipName = namedtuple("ClipName",["name","affect","line_number"])
StmtSourceFile = namedtuple("SourceFile",["filename","line_number"]) StmtSourceFile = namedtuple("SourceFile",["filename","line_number"])
StmtRemark = namedtuple("Remark",["text","line_number"]) StmtRemark = namedtuple("Remark",["text","line_number"])
StmtEffectsName = namedtuple("EffectsName",["name","line_number"]) StmtEffectsName = namedtuple("EffectsName",["name","line_number"])
StmtTrailer = namedtuple("Trailer",["text","line_number"]) StmtSourceUMID = namedtuple("Source",["name","umid","line_number"])
StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"]) StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"])
StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields
StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"]) StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"])
@@ -69,8 +69,8 @@ def _parse_cmx3600_line(line, line_number):
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(">>> SOURCE"):
return _parse_trailer_statement(line, line_number) return _parse_source_umid_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:"):
@@ -157,10 +157,11 @@ def _parse_columns_for_standard_form(line, event_field_length, source_field_leng
source_out=column_strings[12].strip(), source_out=column_strings[12].strip(),
record_in=column_strings[14].strip(), record_in=column_strings[14].strip(),
record_out=column_strings[16].strip(), record_out=column_strings[16].strip(),
line_number=line_number) line_number=line_number,
format=source_field_length)
def _parse_trailer_statement(line, line_number): def _parse_source_umid_statement(line, line_number):
trimmed = line[3:].strip() trimmed = line[3:].strip()
return StmtTrailer(trimmed, line_number=line_number) return StmtSourceUMID(name=None, umid=None, line_number=line_number)

View File

@@ -70,12 +70,12 @@ class Transition:
@property @property
def key_background(self): def key_background(self):
"`True` if this edit is a key background." "`True` if this edit is a key background."
return self.transition == KeyBackground return self.transition == Transition.KeyBackground
@property @property
def key_foreground(self): def key_foreground(self):
"`True` if this edit is a key foreground." "`True` if this edit is a key foreground."
return self.transition == Key return self.transition == Transition.Key
@property @property
def key_out(self): def key_out(self):
@@ -83,4 +83,4 @@ class Transition:
`True` if this edit is a key out. This material will removed from `True` if this edit is a key out. This material will removed from
the key foreground and replaced with the key background. the key foreground and replaced with the key background.
""" """
return self.transition == KeyOut return self.transition == Transition.KeyOut

View File

@@ -4,16 +4,22 @@ with open("README.md", "r") as fh:
long_description = fh.read() long_description = fh.read()
setup(name='pycmx', setup(name='pycmx',
version='0.8', version='1.1.3',
author='Jamie Hardt', author='Jamie Hardt',
author_email='jamiehardt@me.com', author_email='jamiehardt@me.com',
description='CMX 3600 Edit Decision List Parser', description='CMX 3600 Edit Decision List Parser',
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
long_description=long_description, long_description=long_description,
url='https://github.com/iluvcapra/pycmx', url='https://github.com/iluvcapra/pycmx',
classifiers=['Development Status :: 4 - Beta', classifiers=['Development Status :: 5 - Production/Stable',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Topic :: Multimedia', 'Topic :: Multimedia',
'Topic :: Multimedia :: Video', 'Topic :: Multimedia :: Video',
'Topic :: Text Processing'], 'Topic :: Text Processing',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10'
],
packages=['pycmx']) packages=['pycmx'])

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import test_parse

View File

@@ -18,14 +18,15 @@ class TestParse(TestCase):
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): for fn, count in zip(type(self).files, counts):
with open(f"tests/edls/{fn}" ,'r') as f: with open("tests/edls/" + fn ,'r') as f:
edl = pycmx.parse_cmx3600(f) edl = pycmx.parse_cmx3600(f)
actual = len( list( edl.events )) actual = len( list( edl.events ))
self.assertTrue( actual == count , f"expected {count} in file {fn} but found {actual}") self.assertTrue( actual == count ,
"expected %i in file %s but found %i" % (count, fn, actual))
def test_list_sanity(self): def test_list_sanity(self):
for fn in type(self).files: for fn in type(self).files:
with open(f"tests/edls/{fn}" ,'r') as f: with open("tests/edls/" + fn ,'r') as f:
edl = pycmx.parse_cmx3600(f) edl = pycmx.parse_cmx3600(f)
self.assertTrue( type(edl.title) is str ) self.assertTrue( type(edl.title) is str )
self.assertTrue( len(edl.title) > 0 ) self.assertTrue( len(edl.title) > 0 )
@@ -33,7 +34,8 @@ class TestParse(TestCase):
def test_event_sanity(self): def test_event_sanity(self):
for fn in type(self).files: for fn in type(self).files:
with open(f"tests/edls/{fn}" ,'r') as f: path = "tests/edls/" + fn
with open(path ,'r') as f:
edl = pycmx.parse_cmx3600(f) edl = pycmx.parse_cmx3600(f)
for index, event in enumerate(edl.events): for index, event in enumerate(edl.events):
self.assertTrue( len(event.edits) > 0 ) self.assertTrue( len(event.edits) > 0 )
@@ -64,6 +66,7 @@ class TestParse(TestCase):
self.assertFalse( events[0].edits[0].channels.a1) self.assertFalse( events[0].edits[0].channels.a1)
self.assertTrue( events[0].edits[0].channels.a2) 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.get_audio_channel(7) )
self.assertTrue( events[2].edits[0].channels.audio)
def test_multi_edit_events(self): def test_multi_edit_events(self):
@@ -104,3 +107,7 @@ class TestParse(TestCase):
edl = pycmx.parse_cmx3600(f) edl = pycmx.parse_cmx3600(f)
events = list(edl.events) events = list(edl.events)
self.assertEqual( events[4].edits[1].transition.name , "CROSS DISSOLVE" ) self.assertEqual( events[4].edits[1].transition.name , "CROSS DISSOLVE" )
# add test for edit_list.channels