#! /usr/bin/env python
"""
filesequence - A parsing object representing sequential files for fileseq.
"""
import os
from glob import iglob
from itertools import imap, ifilter
from fileseq.exceptions import ParseException, FileSeqException
from fileseq.constants import PAD_MAP, DISK_RE, SPLIT_RE, PRINTF_SYNTAX_PADDING_RE
from fileseq.frameset import FrameSet
[docs]class FileSequence(object):
""":class:`FileSequence` represents an ordered sequence of files.
:type sequence: str
:param sequence: (ie: dir/path.1-100#.ext)
"""
def __init__(self, sequence):
if not hasattr(self, '_frameSet'):
self._frameSet = None
try:
# the main case, padding characters in the path.1-100#.exr
path, frames, self._pad, self._ext = SPLIT_RE.split(sequence, 1)
self._dir, self._base = os.path.split(path)
self._frameSet = FrameSet(frames)
except ValueError:
# edge case 1; we've got an invalid pad
for placeholder in PAD_MAP.keys():
if placeholder in sequence:
msg = "Failed to parse FileSequence: {0}"
raise ParseException(msg.format(sequence))
# edge case 2; we've got a single frame of a sequence
a_frame = DISK_RE.match(sequence)
if a_frame:
self._dir, self._base, frames, self._ext = a_frame.groups()
# edge case 3: we've got a single versioned file, not a sequence
if frames and not self._base.endswith('.'):
self._base = self._base + frames
self._pad = ''
elif not frames:
self._pad = ''
self._frameSet = None
else:
self._frameSet = FrameSet(frames)
if self._frameSet:
self._pad = FileSequence.getPaddingChars(len(frames))
else:
self._pad = ''
self._frameSet = None
# edge case 4; we've got a solitary file, not a sequence
else:
path, self._ext = os.path.splitext(sequence)
self._dir, self._base = os.path.split(path)
self._pad = ''
if self._dir:
if not self._dir.endswith(os.sep):
self._dir += os.sep
else:
self._dir = ''
self._zfill = self.__class__.getPaddingNum(self._pad)
[docs] def split(self):
"""
Split the :class:`FileSequence` into contiguous pieces and return them
as a list of :class:`FileSequence` instances.
:rtype: list
"""
result = []
for frange in self.frameRange().split(","):
result.append(FileSequence(''.join(
(self._dir, self._base, frange, self._pad, self._ext))))
return result
[docs] def dirname(self):
"""
Return the directory name of the sequence.
:rtype: str
"""
return self._dir
[docs] def setDirname(self, dirname):
"""
Set a new directory name for the sequence.
:type dirname: str
:param dirname: the new directory name
:rtype: None
"""
self._dir = dirname
[docs] def basename(self):
"""
Return the basename of the sequence.
:rtype: str
"""
return self._base
[docs] def setBasename(self, base):
"""
Set a new basename for the sequence.
:type base: str
:param base: the new base name
:rtype: None
"""
self._base = base
[docs] def padding(self):
"""
Return the the padding characters in the sequence.
:rtype: str
"""
return self._pad
[docs] def setPadding(self, padding):
"""
Set new padding characters for the sequence.
i.e. "#" or "@@@" or '%04d', or an empty string to disable range formatting.
:type padding: str
:rtype: None
"""
self._pad = padding
self._zfill = self.__class__.getPaddingNum(self._pad)
[docs] def frameSet(self):
"""
Return the :class:`fileseq.frameset.FrameSet` of the sequence if specified,
otherwise None.
:rtype: :class:`fileseq.frameset.FrameSet` or None
"""
return self._frameSet
[docs] def setFrameSet(self, frameSet):
"""
Set a new :class:`fileseq.frameset.FrameSet` for the sequence.
:param frameSet: the new :class:`fileseq.frameset.FrameSet` object
:rtype: None
"""
self._frameSet = frameSet
[docs] def extension(self):
"""
Return the file extension of the sequence, including leading period.
:rtype: str
"""
return self._ext
[docs] def setExtension(self, ext):
"""
Set a new file extension for the sequence.
.. note::
A leading period will be added if none is provided.
:param ext: the new file extension
:rtype: None
"""
if ext[0] != ".":
ext = "." + ext
self._ext = ext
[docs] def setExtention(self, ext):
"""
Deprecated: use :meth:`setExtension`.
"""
import warnings
msg = "the setExtention method is deprecated, please use setExtension"
warnings.warn(msg)
self.setExtension(ext)
[docs] def frameRange(self):
"""
Returns the string formatted frame range of the sequence.
Will return an empty string if the sequence has no frame pattern.
:rtype: str
"""
if not self._frameSet:
return ''
return self._frameSet.frameRange(self._zfill)
[docs] def setFrameRange(self, frange):
"""
Set a new frame range for the sequence.
:param frange: a properly formatted frame range, as per :class:`fileseq.frameset.FrameSet`
:rtype: None
"""
self._frameSet = FrameSet(frange)
[docs] def invertedFrameRange(self):
"""
Returns the inverse string formatted frame range of the sequence.
Will return an empty string if the sequence has no frame pattern.
:rtype: str
"""
if not self._frameSet:
return ''
return self._frameSet.invertedFrameRange(self._zfill)
[docs] def start(self):
"""
Returns the start frame of the sequence's :class:`fileseq.frameset.FrameSet`.
Will return 0 if the sequence has no frame pattern.
:rtype: int
"""
if not self._frameSet:
return 0
return self._frameSet.start()
[docs] def end(self):
"""
Returns the end frame of the sequences :class:`fileseq.frameset.FrameSet`.
Will return 0 if the sequence has no frame pattern.
:rtype: int
"""
if not self._frameSet:
return 0
return self._frameSet.end()
[docs] def zfill(self):
"""
Returns the zfill depth (ie the number of zeroes to pad with).
:rtype: int
"""
return self._zfill
[docs] def frame(self, frame):
"""
Return a path go the given frame in the sequence. Integer or string
digits are treated as a frame number and padding is applied, all other
values are passed though.
:Example:
>>> seq.frame(1)
/foo/bar.0001.exr
>>> seq.frame("#")
/foo/bar.#.exr
:param frame: the desired frame number (int/str) or a char to pass through (ie. #)
:rtype: str
"""
try:
zframe = str(int(frame)).zfill(self._zfill)
except ValueError:
zframe = frame
# There may have been no placeholder for frame IDs in
# the sequence, in which case we don't want to insert
# a frame ID
if self._zfill == 0:
zframe = ""
return "".join((self._dir, self._base, zframe, self._ext))
[docs] def index(self, idx):
"""
Return the path to the file at the given index.
:type idx: int
:param idx: the desired index
:rtype: str
"""
if not self._frameSet:
return str(self)
return self.frame(self._frameSet[idx])
[docs] def __iter__(self):
"""
Allow iteration over the path or paths this :class:`FileSequence`
represents.
:rtype: generator
"""
# If there is no frame range, or there is no padding
# characters, then we only want to represent a single path
if not self._frameSet or not self._zfill:
yield str(self)
return
for f in self._frameSet:
yield self.frame(f)
[docs] def __getitem__(self, idx):
"""
Allows access via index to the underlying :class:`fileseq.frameset.FrameSet`.
:type idx: int
:param idx: the desired index
:rtype: int
"""
return self.index(idx)
[docs] def __len__(self):
"""
The length (number of files) represented by this :class:`FileSequence`.
:rtype: int
"""
if not self._frameSet or not self._zfill:
return 1
return len(self._frameSet)
def __str__(self):
"""
String representation of this :class:`FileSequence`.
:rtype: str
"""
frameSet = str(self._frameSet or "")
return "".join((
self._dir,
self._base,
frameSet,
self._pad if frameSet else "",
self._ext))
@staticmethod
[docs] def yield_sequences_in_list(paths):
"""
Yield the discrete sequences within paths. This does not try to
determine if the files actually exist on disk, it assumes you already
know that.
:param paths: a list of paths
:rtype: generator
"""
seqs = {}
_check = DISK_RE.match
for match in ifilter(None, imap(_check, paths)):
dirname, basename, frame, ext = match.groups()
if not basename and not ext:
continue
key = (dirname, basename, ext)
seqs.setdefault(key, set())
if frame:
seqs[key].add(frame)
for (dirname, basename, ext), frames in seqs.iteritems():
# build the FileSequence behind the scenes, rather than dupe work
seq = FileSequence.__new__(FileSequence)
seq._dir = dirname or ''
seq._base = basename or ''
seq._ext = ext or ''
if frames:
seq._frameSet = FrameSet(set(imap(int, frames))) if frames else None
seq._pad = FileSequence.getPaddingChars(min(imap(len, frames)))
else:
seq._frameSet = None
seq._pad = ''
seq.__init__(str(seq))
yield seq
@staticmethod
[docs] def findSequencesInList(paths):
"""
Returns the list of discrete sequences within paths. This does not try
to determine if the files actually exist on disk, it assumes you
already know that.
:param paths: a list of paths
:rtype: list
"""
return list(FileSequence.yield_sequences_in_list(paths))
@staticmethod
[docs] def findSequencesOnDisk(dirpath, include_hidden=False):
"""
Yield the sequences found in the given directory.
:param dirpath: directory to scan
:type include_hidden: bool
:param include_hidden: if true, show .hidden files as well
:rtype: list
"""
# reserve some functions we're going to need quick access to
_not_hidden = lambda f: not f.startswith('.')
_isfile = os.path.isfile
_join = os.path.join
# collapse some generators to get us the files that match our regex
files = os.listdir(dirpath)
if not include_hidden:
files = ifilter(_not_hidden, files)
files = (_join(dirpath, f) for f in files)
files = ifilter(_isfile, files)
return list(FileSequence.yield_sequences_in_list(files))
@staticmethod
[docs] def findSequenceOnDisk(pattern):
"""
Search for a specific sequence on disk.
:Example:
>>> findSequenceOnDisk("seq/bar#.exr") # or any fileseq pattern
:param pattern: the sequence pattern being searched for
:rtype: str
:raises: :class:`fileseq.exceptions.FileSeqException` if no sequence is found on disk
"""
seq = FileSequence(pattern)
if seq.frameRange() == '' and seq.padding() == '':
if os.path.isfile(pattern):
return seq
patt = seq.format('{dirname}{basename}*{extension}')
ext = seq.extension()
basename = seq.basename()
matches = FileSequence.yield_sequences_in_list(iglob(patt))
for match in matches:
if match.basename() == basename and match.extension() == ext:
return match
msg = 'no sequence found on disk matching {0}'
raise FileSeqException(msg.format(pattern))
@staticmethod
[docs] def getPaddingChars(num):
"""
Given a particular amount of padding, return the proper padding characters.
"""
if num == 0:
return "@"
if num % 4 == 0:
return "#" * (num // 4)
else:
return "@" * num
@staticmethod
[docs] def getPaddingNum(chars):
"""
Given a supported group of padding characters, return the amount of padding.
:type chars: str
:param chars: a supported group of padding characters
:rtype: int
:raises: ValueError if unsupported padding character is detected
"""
match = PRINTF_SYNTAX_PADDING_RE.match(chars)
if match:
return int(match.group(1))
try:
return sum([PAD_MAP[char] for char in chars])
except KeyError:
msg = "Detected an unsupported padding character: \"{}\"."
msg += " Supported padding characters: {} or printf syntax padding"
msg += " %<int>d"
raise ValueError(msg.format(char, str(PAD_MAP.keys())))