From e19bb3c5c5023bc3f8f5939473e2cf9f03a4dadb Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Wed, 31 May 2023 11:33:15 -0700 Subject: [PATCH 1/6] Update __init__.py Made first line of module documentation shorter, it gets cut off on pypi. --- pycmx/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pycmx/__init__.py b/pycmx/__init__.py index 4a20f1c..97d75d2 100644 --- a/pycmx/__init__.py +++ b/pycmx/__init__.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """ -pycmx is a module for parsing CMX 3600-style EDLs. For more information and -examples see README.md +pycmx is a parser for CMX 3600-style EDLs. This module (c) 2022 Jamie Hardt. For more information on your rights to copy and reuse this software, refer to the LICENSE file included with the From 2b38d8aaf9a750aa87cf7d7b5a3a8f60ae6fe26b Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Wed, 31 May 2023 11:34:40 -0700 Subject: [PATCH 2/6] Update __init__.py Bumped copyright year and version number. --- pycmx/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycmx/__init__.py b/pycmx/__init__.py index 97d75d2..80e4951 100644 --- a/pycmx/__init__.py +++ b/pycmx/__init__.py @@ -2,12 +2,12 @@ """ pycmx is a parser for CMX 3600-style EDLs. -This module (c) 2022 Jamie Hardt. For more information on your rights to +This module (c) 2023 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.0' +__version__ = '1.2.1' from .parse_cmx_events import parse_cmx3600 from .transition import Transition From 179808fbf20aae71cc255de842dbffd8d12d4349 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Wed, 31 May 2023 11:59:26 -0700 Subject: [PATCH 3/6] Added type annotations and doc fixes --- docs/source/conf.py | 9 +++++---- pycmx/edit.py | 28 +++++++++++++++------------- pycmx/edit_list.py | 22 +++++++++++----------- pycmx/event.py | 13 +++++++------ 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 708e879..01ca47d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -15,18 +15,19 @@ import os import sys sys.path.insert(0, os.path.abspath('../..')) -print(sys.path) + +import pycmx # -- Project information ----------------------------------------------------- project = u'pycmx' -copyright = u'2022, Jamie Hardt' +copyright = u'(c) 2023, Jamie Hardt' author = u'Jamie Hardt' # The short X.Y version -version = u'' +version = pycmx.__version__ # The full version, including alpha/beta/rc tags -release = u'' +release = pycmx.__version__ # -- General configuration --------------------------------------------------- diff --git a/pycmx/edit.py b/pycmx/edit.py index bbbbab3..3f60b3c 100644 --- a/pycmx/edit.py +++ b/pycmx/edit.py @@ -3,7 +3,9 @@ from .transition import Transition from .channel_map import ChannelMap -from .parse_cmx_statements import StmtEffectsName +# from .parse_cmx_statements import StmtEffectsName + +from typing import Optional class Edit: """ @@ -18,7 +20,7 @@ class Edit: self.trans_name_statement = trans_name_statement @property - def line_number(self): + 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 @@ -27,7 +29,7 @@ class Edit: return self.edit_statement.line_number @property - def channels(self): + def channels(self) -> ChannelMap: """ Get the :obj:`ChannelMap` object associated with this Edit. """ @@ -38,7 +40,7 @@ class Edit: return cm @property - def transition(self): + def transition(self) -> Transition: """ Get the :obj:`Transition` object associated with this edit. """ @@ -48,14 +50,14 @@ class Edit: return Transition(self.edit_statement.trans, self.edit_statement.trans_op, None) @property - def source_in(self): + def source_in(self) -> str: """ Get the source in timecode. """ return self.edit_statement.source_in @property - def source_out(self): + def source_out(self) -> str: """ Get the source out timecode. """ @@ -63,7 +65,7 @@ class Edit: return self.edit_statement.source_out @property - def record_in(self): + def record_in(self) -> str: """ Get the record in timecode. """ @@ -71,7 +73,7 @@ class Edit: return self.edit_statement.record_in @property - def record_out(self): + def record_out(self) -> str: """ Get the record out timecode. """ @@ -79,7 +81,7 @@ class Edit: return self.edit_statement.record_out @property - def source(self): + def source(self) -> str: """ 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. @@ -87,21 +89,21 @@ class Edit: return self.edit_statement.source @property - def black(self): + def black(self) -> bool: """ Black video or silence should be used as the source for this event. """ return self.source == "BL" @property - def aux_source(self): + def aux_source(self) -> bool: """ An auxiliary source is the source of this event. """ return self.source == "AX" @property - def source_file(self): + def source_file(self) -> Optional[str]: """ Get the source file, as attested by a "* SOURCE FILE" remark on the EDL. This will return None if the information is not present. @@ -112,7 +114,7 @@ class Edit: return self.source_file_statement.filename @property - def clip_name(self): + def clip_name(self) -> Optional[str]: """ 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 diff --git a/pycmx/edit_list.py b/pycmx/edit_list.py index a8c27f8..b126b69 100644 --- a/pycmx/edit_list.py +++ b/pycmx/edit_list.py @@ -5,21 +5,21 @@ from .parse_cmx_statements import (StmtUnrecognized, StmtFCM, StmtEvent, StmtSou from .event import Event from .channel_map import ChannelMap +from typing import Generator + class EditList: """ - Represents an entire edit decision list as returned by `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:] - @property - def format(self): + def format(self) -> str: """ - The detected format of the EDL. Possible values are: `3600`,`File32`, - `File128`, and `unknown` + 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) @@ -37,7 +37,7 @@ class EditList: @property - def channels(self): + def channels(self) -> ChannelMap: """ Return the union of every channel channel. """ @@ -51,7 +51,7 @@ class EditList: @property - def title(self): + def title(self) -> str: """ The title of this edit list. """ @@ -59,7 +59,7 @@ class EditList: @property - def unrecognized_statements(self): + def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]: """ A generator for all the unrecognized statements in the list. """ @@ -69,7 +69,7 @@ class EditList: @property - def events(self): + def events(self) -> Generator[Event, None, None]: 'A generator for all the events in the edit list' is_drop = None current_event_num = None @@ -97,7 +97,7 @@ class EditList: yield Event(statements=event_statements) @property - def sources(self): + def sources(self) -> Generator[StmtSourceUMID, None, None]: """ A generator for all of the sources in the list """ diff --git a/pycmx/event.py b/pycmx/event.py index f43e8f8..da9f197 100644 --- a/pycmx/event.py +++ b/pycmx/event.py @@ -1,26 +1,28 @@ # pycmx -# (c) 2018 Jamie Hardt +# (c) 2023 Jamie Hardt from .parse_cmx_statements import (StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtEffectsName) from .edit import Edit +from typing import List, Generator + class Event: """ - Represents a collection of :class:`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): + def number(self) -> int: """ Return the event number. """ return int(self._edit_statements()[0].event) @property - def edits(self): + def edits(self) -> List[Edit]: """ 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 @@ -63,11 +65,10 @@ class Event: except IndexError: the_zip.append([None] * len(edits_audio) ) - return [ Edit(e1[0],e1[1],n1,s1,u1) for (e1,n1,s1,u1) in zip(*the_zip) ] @property - def unrecognized_statements(self): + def unrecognized_statements(self) -> Generator[StmtUnrecognized, None, None]: """ A generator for all the unrecognized statements in the event. """ From a6f042c76fbc4aad7f2ee35b541bff4fc9b81daf Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Wed, 31 May 2023 17:50:03 -0700 Subject: [PATCH 4/6] More typing cleanups --- pycmx/event.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/pycmx/event.py b/pycmx/event.py index da9f197..3ff0df7 100644 --- a/pycmx/event.py +++ b/pycmx/event.py @@ -2,9 +2,9 @@ # (c) 2023 Jamie Hardt from .parse_cmx_statements import (StmtEvent, StmtClipName, StmtSourceFile, StmtAudioExt, StmtUnrecognized, StmtEffectsName) -from .edit import Edit +from .edit import Edit -from typing import List, Generator +from typing import List, Generator, Optional, Tuple, Any class Event: """ @@ -32,17 +32,19 @@ class Event: clip_names = self._clip_name_statements() source_files= self._source_file_statements() - the_zip = [edits_audio] + the_zip: List[List[Any]] = [edits_audio] if len(edits_audio) == 2: - cn = [None, None] + start_name: Optional[StmtClipName] = None + end_name: Optional[StmtClipName] = None + for clip_name in clip_names: if clip_name.affect == 'from': - cn[0] = clip_name + start_name = clip_name elif clip_name.affect == 'to': - cn[1] = clip_name + end_name = clip_name - the_zip.append(cn) + the_zip.append([start_name, end_name]) else: if len(edits_audio) == len(clip_names): the_zip.append(clip_names) @@ -59,13 +61,17 @@ class Event: # attach trans name to last event try: trans_statement = self._trans_name_statements()[0] - trans_names = [None] * (len(edits_audio) - 1) + trans_names: List[Optional[Any]] = [None] * (len(edits_audio) - 1) trans_names.append(trans_statement) the_zip.append(trans_names) except IndexError: the_zip.append([None] * len(edits_audio) ) - return [ Edit(e1[0],e1[1],n1,s1,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]: @@ -76,19 +82,19 @@ class Event: if type(s) is StmtUnrecognized: yield s - def _trans_name_statements(self): + def _trans_name_statements(self) -> List[StmtEffectsName]: return [s for s in self.statements if type(s) is StmtEffectsName] - def _edit_statements(self): + def _edit_statements(self) -> List[StmtEvent]: return [s for s in self.statements if type(s) is StmtEvent] - def _clip_name_statements(self): + def _clip_name_statements(self) -> List[StmtClipName]: return [s for s in self.statements if type(s) is StmtClipName] - def _source_file_statements(self): + 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): + 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) From 51ed92f5dfdde5d790b0d7d172ae66a4c1e81bc9 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Wed, 31 May 2023 18:10:44 -0700 Subject: [PATCH 5/6] More typing --- pycmx/channel_map.py | 28 ++++++++++++++-------------- pycmx/parse_cmx_events.py | 15 +++++++-------- pycmx/parse_cmx_statements.py | 10 ++++++---- pycmx/transition.py | 24 ++++++++++++++---------- 4 files changed, 41 insertions(+), 36 deletions(-) diff --git a/pycmx/channel_map.py b/pycmx/channel_map.py index a7071ef..f52cc4f 100644 --- a/pycmx/channel_map.py +++ b/pycmx/channel_map.py @@ -2,7 +2,7 @@ # (c) 2018 Jamie Hardt from re import (compile, match) -from typing import Dict, Tuple +from typing import Dict, Tuple, Generator class ChannelMap: """ @@ -24,62 +24,62 @@ class ChannelMap: self.v = v @property - def video(self): + def video(self) -> bool: 'True if video is included' return self.v @property - def audio(self): + def audio(self) -> bool: 'True if an audio channel is included' return len(self._audio_channel_set) > 0 @property - def channels(self): + def channels(self) -> Generator[int, None, None]: 'A generator for each audio channel' for c in self._audio_channel_set: yield c @property - def a1(self): + def a1(self) -> bool: """True if A1 is included""" return self.get_audio_channel(1) @a1.setter - def a1(self,val): + def a1(self, val: bool): self.set_audio_channel(1,val) @property - def a2(self): + def a2(self) -> bool: """True if A2 is included""" return self.get_audio_channel(2) @a2.setter - def a2(self,val): + def a2(self, val: bool): self.set_audio_channel(2,val) @property - def a3(self): + def a3(self) -> bool: """True if A3 is included""" return self.get_audio_channel(3) @a3.setter - def a3(self,val): + def a3(self, val: bool): self.set_audio_channel(3,val) @property - def a4(self): + def a4(self) -> bool: """True if A4 is included""" return self.get_audio_channel(4) @a4.setter - def a4(self,val): + def a4(self,val: bool): self.set_audio_channel(4,val) - def get_audio_channel(self,chan_num): + 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): + 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) diff --git a/pycmx/parse_cmx_events.py b/pycmx/parse_cmx_events.py index 211e2ad..176c0cc 100644 --- a/pycmx/parse_cmx_events.py +++ b/pycmx/parse_cmx_events.py @@ -1,20 +1,19 @@ # pycmx # (c) 2018 Jamie Hardt -from collections import namedtuple +# from collections import namedtuple -from .parse_cmx_statements import (parse_cmx3600_statements, StmtEvent,StmtFCM ) +from .parse_cmx_statements import (parse_cmx3600_statements) from .edit_list import EditList -def parse_cmx3600(f): +from typing import TextIO + +def parse_cmx3600(f: TextIO): """ Parse a CMX 3600 EDL. - Args: - f : a file-like object, anything that's readlines-able. - - Returns: - An :class:`pycmx.edit_list.EditList`. + :param TextIO f: a file-like object, anything that's readlines-able. + :returns: An :class:`pycmx.edit_list.EditList`. """ statements = parse_cmx3600_statements(f) return EditList(statements) diff --git a/pycmx/parse_cmx_statements.py b/pycmx/parse_cmx_statements.py index d391698..957c238 100644 --- a/pycmx/parse_cmx_statements.py +++ b/pycmx/parse_cmx_statements.py @@ -5,6 +5,8 @@ import re import sys from collections import namedtuple from itertools import count +from typing import TextIO, List + from .util import collimate @@ -18,12 +20,12 @@ StmtSourceFile = namedtuple("SourceFile",["filename","line_number"]) StmtRemark = namedtuple("Remark",["text","line_number"]) StmtEffectsName = namedtuple("EffectsName",["name","line_number"]) StmtSourceUMID = namedtuple("Source",["name","umid","line_number"]) -StmtSplitEdit = namedtuple("SplitEdit",["video","magnitue", "line_number"]) +StmtSplitEdit = namedtuple("SplitEdit",["video","magnitude", "line_number"]) StmtMotionMemory = namedtuple("MotionMemory",["source","fps"]) # FIXME needs more fields StmtUnrecognized = namedtuple("Unrecognized",["content","line_number"]) -def parse_cmx3600_statements(file): +def parse_cmx3600_statements(file: TextIO) -> List[object]: """ Return a list of every statement in the file argument. """ @@ -109,7 +111,7 @@ def _parse_extended_audio_channels(line, line_number): else: return StmtUnrecognized(content=line, line_number=line_number) -def _parse_remark(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) elif line.startswith("TO CLIP NAME:"): @@ -119,7 +121,7 @@ def _parse_remark(line, line_number): else: return StmtRemark(text=line, line_number=line_number) -def _parse_effects_name(line, line_number): +def _parse_effects_name(line, line_number) -> StmtEffectsName: name = line[16:].strip() return StmtEffectsName(name=name, line_number=line_number) diff --git a/pycmx/transition.py b/pycmx/transition.py index 988b411..5d2ebe3 100644 --- a/pycmx/transition.py +++ b/pycmx/transition.py @@ -1,5 +1,7 @@ # pycmx -# (c) 2018 Jamie Hardt +# (c) 2023 Jamie Hardt + +from typing import Optional class Transition: """ @@ -19,7 +21,7 @@ class Transition: self.name = name @property - def kind(self): + def kind(self) -> Optional[str]: """ Return the kind of transition: Cut, Wipe, etc """ @@ -37,22 +39,22 @@ class Transition: return Transition.KeyOut @property - def cut(self): + def cut(self) -> bool: "`True` if this transition is a cut." return self.transition == 'C' @property - def dissolve(self): + def dissolve(self) -> bool: "`True` if this traansition is a dissolve." return self.transition == 'D' @property - def wipe(self): + def wipe(self) -> bool: "`True` if this transition is a wipe." return self.transition.startswith('W') @property - def effect_duration(self): + 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. @@ -60,7 +62,7 @@ class Transition: return int(self.operand) @property - def wipe_number(self): + def wipe_number(self) -> Optional[int]: "Wipes are identified by a particular number." if self.wipe: return int(self.transition[1:]) @@ -68,19 +70,21 @@ class Transition: return None @property - def key_background(self): + def key_background(self) -> bool: "`True` if this edit is a key background." return self.transition == Transition.KeyBackground @property - def key_foreground(self): + def key_foreground(self) -> bool: "`True` if this edit is a key foreground." return self.transition == Transition.Key @property - def key_out(self): + def key_out(self) -> bool: """ `True` if this edit is a key out. This material will removed from the key foreground and replaced with the key background. """ return self.transition == Transition.KeyOut + + From b78ae05d8c26f672ecf9057dae7066db354c2a01 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Wed, 31 May 2023 18:53:46 -0700 Subject: [PATCH 6/6] Update pythonpublish.yml Updated to use pypi Trusted Publisher. --- .github/workflows/pythonpublish.yml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index 75091f7..05b274f 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -2,30 +2,32 @@ name: Upload Python Package on: release: - types: [created] + types: [published] workflow_dispatch: - +permissions: + contents: read + id-token: write + jobs: deploy: runs-on: ubuntu-latest + environment: + name: release steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v3 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 -m build . - twine upload dist/* + pip install build + - name: Build package + run: python -m build + - name: pypi-publish + uses: pypa/gh-action-pypi-publish@v1.8.6 - name: Report to Mastodon uses: cbrgm/mastodon-github-action@v1.0.1 with: