#! /usr/bin/env python
"""
frameset - A set-like object representing a frame range for fileseq.
"""
from builtins import str
from builtins import map
import future.utils
import numbers
from collections import Set, Sequence
from fileseq import constants, utils
from fileseq.constants import PAD_MAP, FRANGE_RE, PAD_RE
from fileseq.exceptions import MaxSizeException, ParseException
from fileseq.utils import xfrange, unique, pad
# Issue #44
# Possibly use an alternate range implementation, depending on platform.
from fileseq.utils import range
[docs]class FrameSet(Set):
"""
A :class:`FrameSet` is an immutable representation of the ordered, unique
set of frames in a given frame range.
The frame range can be expressed in the following ways:
- 1-5
- 1-5,10-20
- 1-100x5 (every fifth frame)
- 1-100y5 (opposite of above, fills in missing frames)
- 1-100:4 (same as 1-100x4,1-100x3,1-100x2,1-100)
A :class:`FrameSet` is effectively an ordered frozenset, with
FrameSet-returning versions of frozenset methods:
>>> FrameSet('1-5').union(FrameSet('5-10'))
FrameSet('1-10')
>>> FrameSet('1-5').intersection(FrameSet('5-10'))
FrameSet('5')
Because a FrameSet is hashable, it can be used as the key to a dictionary:
>>> {FrameSet('1-20'): 'good'}
Caveats:
1. Because the internal storage of a :class:`FrameSet` contains the discreet
values of the entire range, an exception will be thrown if the range
exceeds a large reasonable limit, which could lead to huge memory
allocations or memory failures. See :const:`fileseq.constants.MAX_FRAME_SIZE`.
2. All frozenset operations return a normalized :class:`FrameSet`:
internal frames are in numerically increasing order.
3. Equality is based on the contents and order, NOT the frame range
string (there are a finite, but potentially
extremely large, number of strings that can represent any given range,
only a "best guess" can be made).
4. Human-created frame ranges (ie 1-100x5) will be reduced to the
actual internal frames (ie 1-96x5).
5. The "null" :class:`Frameset` (``FrameSet('')``) is now a valid thing
to create, it is required by set operations, but may cause confusion
as both its start and end methods will raise IndexError. The
:meth:`is_null`
property has been added to allow you to guard against this.
Args:
frange (str or FrameSet or collections.Iterable): the frame range
as a string (ie "1-100x5")
Raises:
:class:`.ParseException`: if the frame range
(or a portion of it) could not be parsed.
:class:`fileseq.exceptions.MaxSizeException`: if the range exceeds
`fileseq.constants.MAX_FRAME_SIZE`
"""
__slots__ = ('_frange', '_items', '_order')
def __new__(cls, *args, **kwargs):
"""
Initialize the :class:`FrameSet` object.
Args:
frange (str or :class:`FrameSet`): the frame range as a string (ie "1-100x5")
Raises:
:class:`.ParseException`: if the frame range
(or a portion of it) could not be parsed.
:class:`fileseq.exceptions.MaxSizeException`: if the range exceeds
`fileseq.constants.MAX_FRAME_SIZE`
"""
self = super(cls, FrameSet).__new__(cls)
return self
def __init__(self, frange):
"""Initialize the :class:`FrameSet` object.
"""
# if the user provides anything but a string, short-circuit the build
if not isinstance(frange, future.utils.string_types):
# if it's apparently a FrameSet already, short-circuit the build
if set(dir(frange)).issuperset(self.__slots__):
for attr in self.__slots__:
setattr(self, attr, getattr(frange, attr))
return
# if it's inherently disordered, sort and build
elif isinstance(frange, Set):
self._maxSizeCheck(frange)
self._items = frozenset(map(int, frange))
self._order = tuple(sorted(self._items))
self._frange = FrameSet.framesToFrameRange(
self._order, sort=False, compress=False)
return
# if it's ordered, find unique and build
elif isinstance(frange, Sequence):
self._maxSizeCheck(frange)
items = set()
order = unique(items, map(int, frange))
self._order = tuple(order)
self._items = frozenset(items)
self._frange = FrameSet.framesToFrameRange(
self._order, sort=False, compress=False)
return
# in all other cases, cast to a string
else:
try:
frange = utils.asString(frange)
except Exception as err:
msg = 'Could not parse "{0}": cast to string raised: {1}'
raise ParseException(msg.format(frange, err))
# we're willing to trim padding characters from consideration
# this translation is orders of magnitude faster than prior method
if future.utils.PY2:
frange = bytes(frange).translate(None, ''.join(PAD_MAP.keys()))
self._frange = utils.asString(frange)
else:
frange = str(frange)
for key in PAD_MAP:
frange = frange.replace(key, u'')
self._frange = utils.asString(frange)
# because we're acting like a set, we need to support the empty set
if not self._frange:
self._items = frozenset()
self._order = tuple()
return
# build the mutable stores, then cast to immutable for storage
items = set()
order = []
maxSize = constants.MAX_FRAME_SIZE
for part in self._frange.split(","):
# this is to deal with leading / trailing commas
if not part:
continue
# parse the partial range
start, end, modifier, chunk = FrameSet._parse_frange_part(part)
# handle batched frames (1-100x5)
if modifier == 'x':
frames = xfrange(start, end, chunk, maxSize=maxSize)
frames = [f for f in frames if f not in items]
self._maxSizeCheck(len(frames) + len(items))
order.extend(frames)
items.update(frames)
# handle staggered frames (1-100:5)
elif modifier == ':':
for stagger in range(chunk, 0, -1):
frames = xfrange(start, end, stagger, maxSize=maxSize)
frames = [f for f in frames if f not in items]
self._maxSizeCheck(len(frames) + len(items))
order.extend(frames)
items.update(frames)
# handle filled frames (1-100y5)
elif modifier == 'y':
not_good = frozenset(xfrange(start, end, chunk, maxSize=maxSize))
frames = xfrange(start, end, 1, maxSize=maxSize)
frames = (f for f in frames if f not in not_good)
frames = [f for f in frames if f not in items]
self._maxSizeCheck(len(frames) + len(items))
order.extend(frames)
items.update(frames)
# handle full ranges and single frames
else:
frames = xfrange(start, end, 1 if start < end else -1, maxSize=maxSize)
frames = [f for f in frames if f not in items]
self._maxSizeCheck(len(frames) + len(items))
order.extend(frames)
items.update(frames)
# lock the results into immutable internals
# this allows for hashing and fast equality checking
self._items = frozenset(items)
self._order = tuple(order)
@property
def is_null(self):
"""
Read-only access to determine if the :class:`FrameSet` is the null or
empty :class:`FrameSet`.
Returns:
bool:
"""
return not (self._frange and self._items and self._order)
@property
def frange(self):
"""
Read-only access to the frame range used to create this :class:`FrameSet`.
Returns:
frozenset:
"""
return self._frange
@property
def items(self):
"""
Read-only access to the unique frames that form this :class:`FrameSet`.
Returns:
frozenset:
"""
return self._items
@property
def order(self):
"""
Read-only access to the ordered frames that form this :class:`FrameSet`.
Returns:
tuple:
"""
return self._order
[docs] @classmethod
def from_iterable(cls, frames, sort=False):
"""
Build a :class:`FrameSet` from an iterable of frames.
Args:
frames (collections.Iterable): an iterable object containing frames as integers
sort (bool): True to sort frames before creation, default is False
Returns:
:class:`FrameSet`:
"""
return FrameSet(sorted(frames) if sort else frames)
@classmethod
def _cast_to_frameset(cls, other):
"""
Private method to simplify comparison operations.
Args:
other (:class:`FrameSet` or set or frozenset or or iterable): item to be compared
Returns:
:class:`FrameSet`
Raises:
:class:`NotImplemented`: if a comparison is impossible
"""
if isinstance(other, FrameSet):
return other
try:
return FrameSet(other)
except Exception:
return NotImplemented
[docs] def index(self, frame):
"""
Return the index of the given frame number within the :class:`FrameSet`.
Args:
frame (int): the frame number to find the index for
Returns:
int:
Raises:
:class:`ValueError`: if frame is not in self
"""
return self.order.index(frame)
[docs] def frame(self, index):
"""
Return the frame at the given index.
Args:
index (int): the index to find the frame for
Returns:
int:
Raises:
:class:`IndexError`: if index is out of bounds
"""
return self.order[index]
[docs] def hasFrame(self, frame):
"""
Check if the :class:`FrameSet` contains the frame.
Args:
frame (int): the frame number to search for
Returns:
bool:
"""
return frame in self
[docs] def start(self):
"""
The first frame in the :class:`FrameSet`.
Returns:
int:
Raises:
:class:`IndexError`: (with the empty :class:`FrameSet`)
"""
return self.order[0]
[docs] def end(self):
"""
The last frame in the :class:`FrameSet`.
Returns:
int:
Raises:
:class:`IndexError`: (with the empty :class:`FrameSet`)
"""
return self.order[-1]
[docs] def isConsecutive(self):
"""
Return whether the frame range represents consecutive integers,
as opposed to having a stepping >= 2
Examples:
>>> FrameSet('1-100').isConsecutive()
True
>>> FrameSet('1-100x2').isConsecutive()
False
>>> FrameSet('1-50,60-100').isConsecutive()
False
Returns:
bool:
"""
return len(self) == abs(self.end()-self.start()) + 1
[docs] def frameRange(self, zfill=0):
"""
Return the frame range used to create this :class:`FrameSet`, padded if
desired.
Examples:
>>> FrameSet('1-100').frameRange()
'1-100'
>>> FrameSet('1-100').frameRange(5)
'00001-00100'
Args:
zfill (int): the width to use to zero-pad the frame range string
Returns:
str:
"""
return FrameSet.padFrameRange(self.frange, zfill)
[docs] def invertedFrameRange(self, zfill=0):
"""
Return the inverse of the :class:`FrameSet` 's frame range, padded if
desired.
The inverse is every frame within the full extent of the range.
Examples:
>>> FrameSet('1-100x2').invertedFrameRange()
'2-98x2'
>>> FrameSet('1-100x2').invertedFrameRange(5)
'00002-00098x2'
If the inverted frame size exceeds `fileseq.constants.MAX_FRAME_SIZE`,
a ``MaxSizeException`` will be raised.
Args:
zfill (int): the width to use to zero-pad the frame range string
Returns:
str:
Raises:
:class:`fileseq.exceptions.MaxSizeException`:
"""
result = []
frames = sorted(self.items)
for idx, frame in enumerate(frames[:-1]):
next_frame = frames[idx + 1]
if next_frame - frame != 1:
r = range(frame + 1, next_frame)
# Check if the next update to the result set
# will exceed out max frame size.
# Prevent memory overflows.
self._maxSizeCheck(len(r) + len(result))
result += r
if not result:
return u''
return FrameSet.framesToFrameRange(
result, zfill=zfill, sort=False, compress=False)
[docs] def normalize(self):
"""
Returns a new normalized (sorted and compacted) :class:`FrameSet`.
Returns:
:class:`FrameSet`:
"""
return FrameSet(FrameSet.framesToFrameRange(
self.items, sort=True, compress=False))
[docs] def __getstate__(self):
"""
Allows for serialization to a pickled :class:`FrameSet`.
Returns:
tuple: (frame range string, )
"""
# we have to special-case the empty FrameSet, because of a quirk in
# Python where __setstate__ will not be called if the return value of
# bool(__getstate__) == False. A tuple with ('',) will return True.
return (self.frange, )
[docs] def __setstate__(self, state):
"""
Allows for de-serialization from a pickled :class:`FrameSet`.
Args:
state (tuple or str or dict): A string/dict can be used for
backwards compatibility
Raises:
ValueError: if state is not an appropriate type
"""
if isinstance(state, tuple):
# this is to allow unpickling of "3rd generation" FrameSets,
# which are immutable and may be empty.
self.__init__(state[0])
elif isinstance(state, future.utils.string_types):
# this is to allow unpickling of "2nd generation" FrameSets,
# which were mutable and could not be empty.
self.__init__(state)
elif isinstance(state, dict):
# this is to allow unpickling of "1st generation" FrameSets,
# when the full __dict__ was stored
if '__frange' in state and '__set' in state and '__list' in state:
self._frange = state['__frange']
self._items = frozenset(state['__set'])
self._order = tuple(state['__list'])
else:
for k in self.__slots__:
setattr(self, k, state[k])
else:
msg = "Unrecognized state data from which to deserialize FrameSet"
raise ValueError(msg)
[docs] def __getitem__(self, index):
"""
Allows indexing into the ordered frames of this :class:`FrameSet`.
Args:
index (int): the index to retrieve
Returns:
int:
Raises:
:class:`IndexError`: if index is out of bounds
"""
return self.order[index]
[docs] def __len__(self):
"""
Returns the length of the ordered frames of this :class:`FrameSet`.
Returns:
int:
"""
return len(self.order)
[docs] def __str__(self):
"""
Returns the frame range string of this :class:`FrameSet`.
Returns:
str:
"""
return self.frange
[docs] def __repr__(self):
"""
Returns a long-form representation of this :class:`FrameSet`.
Returns:
str:
"""
return u'{0}("{1}")'.format(self.__class__.__name__, self.frange)
[docs] def __iter__(self):
"""
Allows for iteration over the ordered frames of this :class:`FrameSet`.
Returns:
generator:
"""
return (i for i in self.order)
[docs] def __reversed__(self):
"""
Allows for reversed iteration over the ordered frames of this
:class:`FrameSet`.
Returns:
generator:
"""
return (i for i in reversed(self.order))
[docs] def __contains__(self, item):
"""
Check if item is a member of this :class:`FrameSet`.
Args:
item (int): the frame number to check for
Returns:
bool:
"""
return item in self.items
[docs] def __hash__(self):
"""
Builds the hash of this :class:`FrameSet` for equality checking and to
allow use as a dictionary key.
Returns:
int:
"""
return hash(self.frange) | hash(self.items) | hash(self.order)
[docs] def __lt__(self, other):
"""
Check if self < other via a comparison of the contents. If other is not
a :class:`FrameSet`, but is a set, frozenset, or is iterable, it will be
cast to a :class:`FrameSet`.
Note:
A :class:`FrameSet` is less than other if the set of its contents are
less, OR if the contents are equal but the order of the items is less.
.. code-block:: python
:caption: Same contents, but (1,2,3,4,5) sorts below (5,4,3,2,1)
>>> FrameSet("1-5") < FrameSet("5-1")
True
Args:
other (:class:`FrameSet`): Can also be an object that can be cast to a :class:`FrameSet`
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.items < other.items or (
self.items == other.items and self.order < other.order)
[docs] def __le__(self, other):
"""
Check if `self` <= `other` via a comparison of the contents.
If `other` is not a :class:`FrameSet`, but is a set, frozenset, or
is iterable, it will be cast to a :class:`FrameSet`.
Args:
other (:class:`FrameSet`): Also accepts an object that can be cast to a :class:`FrameSet`
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.items <= other.items
[docs] def __eq__(self, other):
"""
Check if `self` == `other` via a comparison of the hash of
their contents.
If `other` is not a :class:`FrameSet`, but is a set, frozenset, or
is iterable, it will be cast to a :class:`FrameSet`.
Args:
other (:class:`FrameSet`): Also accepts an object that can be cast to a :class:`FrameSet`
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
if not isinstance(other, FrameSet):
if not hasattr(other, '__iter__'):
return NotImplemented
other = self.from_iterable(other)
this = hash(self.items) | hash(self.order)
that = hash(other.items) | hash(other.order)
return this == that
[docs] def __ne__(self, other):
"""
Check if `self` != `other` via a comparison of the hash of
their contents.
If `other` is not a :class:`FrameSet`, but is a set, frozenset, or
is iterable, it will be cast to a :class:`FrameSet`.
Args:
other (:class:`FrameSet`): Also accepts an object that can be cast to a :class:`FrameSet`
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
is_equals = self == other
if is_equals != NotImplemented:
return not is_equals
return is_equals
[docs] def __ge__(self, other):
"""
Check if `self` >= `other` via a comparison of the contents.
If `other` is not a :class:`FrameSet`, but is a set, frozenset, or
is iterable, it will be cast to a :class:`FrameSet`.
Args:
other (:class:`FrameSet`): Also accepts an object that can be cast to a :class:`FrameSet`
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.items >= other.items
[docs] def __gt__(self, other):
"""
Check if `self` > `other` via a comparison of the contents.
If `other` is not a :class:`FrameSet`, but is a set, frozenset, or
is iterable, it will be cast to a :class:`FrameSet`.
Note:
A :class:`FrameSet` is greater than `other` if the set of its
contents are greater,
OR if the contents are equal but the order is greater.
.. code-block:: python
:caption: Same contents, but (1,2,3,4,5) sorts below (5,4,3,2,1)
>>> FrameSet("1-5") > FrameSet("5-1")
False
Args:
other (:class:`FrameSet`): Also accepts an object that can be cast to a :class:`FrameSet`
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.items > other.items or (
self.items == other.items and self.order > other.order)
[docs] def __and__(self, other):
"""
Overloads the ``&`` operator.
Returns a new :class:`FrameSet` that holds only the
frames `self` and `other` have in common.
Note:
The order of operations is irrelevant:
``(self & other) == (other & self)``
Args:
other (:class:`FrameSet`):
Returns:
:class:`FrameSet`:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.from_iterable(self.items & other.items, sort=True)
__rand__ = __and__
[docs] def __sub__(self, other):
"""
Overloads the ``-`` operator.
Returns a new :class:`FrameSet` that holds only the
frames of `self` that are not in `other.`
Note:
This is for left-hand subtraction (``self - other``).
Args:
other (:class:`FrameSet`):
Returns:
:class:`FrameSet`:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.from_iterable(self.items - other.items, sort=True)
[docs] def __rsub__(self, other):
"""
Overloads the ``-`` operator.
Returns a new :class:`FrameSet` that holds only the
frames of `other` that are not in `self.`
Note:
This is for right-hand subtraction (``other - self``).
Args:
other (:class:`FrameSet`):
Returns:
:class:`FrameSet`:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.from_iterable(other.items - self.items, sort=True)
[docs] def __or__(self, other):
"""
Overloads the ``|`` operator.
Returns a new :class:`FrameSet` that holds all the
frames in `self,` `other,` or both.
Note:
The order of operations is irrelevant:
``(self | other) == (other | self)``
Args:
other (:class:`FrameSet`):
Returns:
:class:`FrameSet`:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.from_iterable(self.items | other.items, sort=True)
__ror__ = __or__
[docs] def __xor__(self, other):
"""
Overloads the ``^`` operator.
Returns a new :class:`FrameSet` that holds all the
frames in `self` or `other` but not both.
Note:
The order of operations is irrelevant:
``(self ^ other) == (other ^ self)``
Args:
other (:class:`FrameSet`):
Returns:
:class:`FrameSet`:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.from_iterable(self.items ^ other.items, sort=True)
__rxor__ = __xor__
[docs] def isdisjoint(self, other):
"""
Check if the contents of :class:self has no common intersection with the
contents of :class:other.
Args:
other (:class:`FrameSet`):
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.items.isdisjoint(other.items)
[docs] def issubset(self, other):
"""
Check if the contents of `self` is a subset of the contents of
`other.`
Args:
other (:class:`FrameSet`):
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.items <= other.items
[docs] def issuperset(self, other):
"""
Check if the contents of `self` is a superset of the contents of
`other.`
Args:
other (:class:`FrameSet`):
Returns:
bool:
:class:`NotImplemented`: if `other` fails to convert to a :class:`FrameSet`
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
return self.items >= other.items
[docs] def union(self, *other):
"""
Returns a new :class:`FrameSet` with the elements of `self` and
of `other`.
Args:
other (:class:`FrameSet`): or objects that can cast to :class:`FrameSet`
Returns:
:class:`FrameSet`:
"""
from_frozenset = self.items.union(*map(set, other))
return self.from_iterable(from_frozenset, sort=True)
[docs] def intersection(self, *other):
"""
Returns a new :class:`FrameSet` with the elements common to `self` and
`other`.
Args:
other (:class:`FrameSet`): or objects that can cast to :class:`FrameSet`
Returns:
:class:`FrameSet`:
"""
from_frozenset = self.items.intersection(*map(set, other))
return self.from_iterable(from_frozenset, sort=True)
[docs] def difference(self, *other):
"""
Returns a new :class:`FrameSet` with elements in `self` but not in
`other`.
Args:
other (:class:`FrameSet`): or objects that can cast to :class:`FrameSet`
Returns:
:class:`FrameSet`:
"""
from_frozenset = self.items.difference(*map(set, other))
return self.from_iterable(from_frozenset, sort=True)
[docs] def symmetric_difference(self, other):
"""
Returns a new :class:`FrameSet` that contains all the elements in either
`self` or `other`, but not both.
Args:
other (:class:`FrameSet`):
Returns:
:class:`FrameSet`:
"""
other = self._cast_to_frameset(other)
if other is NotImplemented:
return NotImplemented
from_frozenset = self.items.symmetric_difference(other.items)
return self.from_iterable(from_frozenset, sort=True)
[docs] def copy(self):
"""
Returns a shallow copy of this :class:`FrameSet`.
Returns:
:class:`FrameSet`:
"""
return FrameSet(utils.asString(self))
@classmethod
def _maxSizeCheck(cls, obj):
"""
Raise a MaxSizeException if ``obj`` exceeds MAX_FRAME_SIZE
Args:
obj (numbers.Number or collection):
Raises:
:class:`fileseq.exceptions.MaxSizeException`:
"""
fail = False
size = 0
if isinstance(obj, numbers.Number):
if obj > constants.MAX_FRAME_SIZE:
fail = True
size = obj
elif hasattr(obj, '__len__'):
size = len(obj)
fail = size > constants.MAX_FRAME_SIZE
if fail:
raise MaxSizeException('Frame size %s > %s (MAX_FRAME_SIZE)'
% (size, constants.MAX_FRAME_SIZE))
[docs] @staticmethod
def isFrameRange(frange):
"""
Return True if the given string is a frame range. Any padding
characters, such as '#' and '@' are ignored.
Args:
frange (str): a frame range to test
Returns:
bool:
"""
# we're willing to trim padding characters from consideration
# this translation is orders of magnitude faster than prior method
if future.utils.PY2:
frange = bytes(frange).translate(None, ''.join(PAD_MAP.keys()))
else:
frange = str(frange)
for key in PAD_MAP:
frange = frange.replace(key, u'')
if not frange:
return True
for part in utils.asString(frange).split(','):
if not part:
continue
try:
FrameSet._parse_frange_part(part)
except ParseException:
return False
return True
[docs] @staticmethod
def padFrameRange(frange, zfill):
"""
Return the zero-padded version of the frame range string.
Args:
frange (str): a frame range to test
zfill (int):
Returns:
str:
"""
def _do_pad(match):
"""
Substitutes padded for unpadded frames.
"""
result = list(match.groups())
result[1] = pad(result[1], zfill)
if result[4]:
result[4] = pad(result[4], zfill)
return u''.join((i for i in result if i))
return PAD_RE.sub(_do_pad, frange)
@staticmethod
def _parse_frange_part(frange):
"""
Internal method: parse a discrete frame range part.
Args:
frange (str): single part of a frame range as a string
(ie "1-100x5")
Returns:
tuple: (start, end, modifier, chunk)
Raises:
:class:`.ParseException`: if the frame range can
not be parsed
"""
match = FRANGE_RE.match(frange)
if not match:
msg = 'Could not parse "{0}": did not match {1}'
raise ParseException(msg.format(frange, FRANGE_RE.pattern))
start, end, modifier, chunk = match.groups()
start = int(start)
end = int(end) if end is not None else start
if end > start and chunk is not None and int(chunk) < 0:
msg = 'Could not parse "{0}: chunk can not be negative'
raise ParseException(msg.format(frange))
chunk = abs(int(chunk)) if chunk is not None else 1
# a zero chunk is just plain illogical
if chunk == 0:
msg = 'Could not parse "{0}": chunk cannot be 0'
raise ParseException(msg.format(frange))
return start, end, modifier, chunk
@staticmethod
def _build_frange_part(start, stop, stride, zfill=0):
"""
Private method: builds a proper and padded
frame range string.
Args:
start (int): first frame
stop (int or None): last frame
stride (int or None): increment
zfill (int): width for zero padding
Returns:
str:
"""
if stop is None:
return u''
pad_start = pad(start, zfill)
pad_stop = pad(stop, zfill)
if stride is None or start == stop:
return u'{0}'.format(pad_start)
elif abs(stride) == 1:
return u'{0}-{1}'.format(pad_start, pad_stop)
else:
return u'{0}-{1}x{2}'.format(pad_start, pad_stop, stride)
[docs] @staticmethod
def framesToFrameRanges(frames, zfill=0):
"""
Converts a sequence of frames to a series of padded
frame range strings.
Args:
frames (collections.Iterable): sequence of frames to process
zfill (int): width for zero padding
Yields:
str:
"""
_build = FrameSet._build_frange_part
curr_start = None
curr_stride = None
curr_frame = None
last_frame = None
curr_count = 0
for curr_frame in frames:
if curr_start is None:
curr_start = curr_frame
last_frame = curr_frame
curr_count += 1
continue
if curr_stride is None:
curr_stride = abs(curr_frame-curr_start)
new_stride = abs(curr_frame-last_frame)
if curr_stride == new_stride:
last_frame = curr_frame
curr_count += 1
elif curr_count == 2 and curr_stride != 1:
yield _build(curr_start, curr_start, None, zfill)
curr_start = last_frame
curr_stride = new_stride
last_frame = curr_frame
else:
yield _build(curr_start, last_frame, curr_stride, zfill)
curr_stride = None
curr_start = curr_frame
last_frame = curr_frame
curr_count = 1
if curr_count == 2 and curr_stride != 1:
yield _build(curr_start, curr_start, None, zfill)
yield _build(curr_frame, curr_frame, None, zfill)
else:
yield _build(curr_start, curr_frame, curr_stride, zfill)
[docs] @staticmethod
def framesToFrameRange(frames, sort=True, zfill=0, compress=False):
"""
Converts an iterator of frames into a
frame range string.
Args:
frames (collections.Iterable): sequence of frames to process
sort (bool): sort the sequence before processing
zfill (int): width for zero padding
compress (bool): remove any duplicates before processing
Returns:
str:
"""
if compress:
frames = unique(set(), frames)
frames = list(frames)
if not frames:
return u''
if len(frames) == 1:
return pad(frames[0], zfill)
if sort:
frames.sort()
return u','.join(FrameSet.framesToFrameRanges(frames, zfill))