Example #1
0
    def setFrameRange(self, frange):
        """
        Set a new frame range for the sequence.

        Args:
            frange (str): a properly formatted frame range, as per :class:`.FrameSet`
        """
        self._frameSet = FrameSet(frange)
Example #2
0
    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)
Example #3
0
    def __init__(self, sequence):
        """
        Initialize the FileSequence.
        :param sequence: string (ie: dir/path.1-100#.ext)
        :return: None
        """
        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 = sum([PAD_MAP[c] for c in self._pad])
Example #4
0
    def __init__(self, sequence):
        """Init the class
        """
        sequence = utils.asString(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:
            self.setDirname(self._dir)

        self._zfill = self.__class__.getPaddingNum(self._pad)
Example #5
0
    def __init__(self, sequence):
        """Init the class
        """
        sequence = utils.asString(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:
                    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 = u''
                    elif not frames:
                        self._pad = u''
                        self._frameSet = None
                    else:
                        self._frameSet = FrameSet(frames)
                        if self._frameSet:
                            self._pad = FileSequence.getPaddingChars(len(frames))
                        else:
                            self._pad = u''
                            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 = u''

        if self._dir:
            self.setDirname(self._dir)

        self._zfill = self.__class__.getPaddingNum(self._pad)
Example #6
0
 def setFrameRange(self, frange):
     """
     Set a new frame range for the sequence.
     :param frange: a properly formatted frame range, as per FrameSet
     :return: None
     """
     self._frameSet = FrameSet(frange)
Example #7
0
    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
Example #8
0
    def __getitem__(self, idx):
        """
        Allows indexing and slicing into the underlying :class:`fileseq.FrameSet`.

        When indexing, a string filepath is returns for the frame.

        When slicing, a new :class:`FileSequence` is returned.
        Slicing outside the range of the sequence results in an
        IndexError

        :type idx: int or slice
        :param idx: the desired index
        :rtype: str or :obj:`FileSequence`
        """
        if not self._frameSet:
            return str(self)

        frames = self._frameSet[idx]

        if not hasattr(idx, 'start'):
            return self.frame(frames)

        fset = FrameSet(frames)
        if fset.is_null:
            raise IndexError("slice is out of range and returns no frames")

        fs = self.copy()
        fs.setFrameSet(fset)
        return fs
Example #9
0
    def __getitem__(self, idx):
        """
        Allows indexing and slicing into the underlying :class:`.FrameSet`

        When indexing, a string filepath is returns for the frame.

        When slicing, a new :class:`FileSequence` is returned.
        Slicing outside the range of the sequence results in an
        IndexError

        Args:
            idx (int or slice): the desired index

        Returns:
            str or :obj:`FileSequence`:

        Raises:
            :class:`IndexError`: If slice is outside the range of the sequence
        """
        if not self._frameSet:
            return futils.native_str(self)

        frames = self._frameSet[idx]

        if not hasattr(idx, 'start'):
            return self.frame(frames)

        fset = FrameSet(frames)
        if fset.is_null:
            raise IndexError("slice is out of range and returns no frames")

        fs = self.copy()
        fs.setFrameSet(fset)
        return fs
Example #10
0
    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)
Example #11
0
    def setFrameRange(self, frange):
        """
        Set a new frame range for the sequence.

        Args:
            frange (str): a properly formatted frame range, as per :class:`.FrameSet`
        """
        self._frameSet = FrameSet(frange)
Example #12
0
 def frames_to_seq(frames, pad_length, decimal_places):
     seq = start_new_seq()
     seq._frameSet = FrameSet(sorted(decimal.Decimal(f) for f in frames))
     seq._frame_pad = cls.getPaddingChars(pad_length, pad_style=pad_style)
     if decimal_places:
         seq._subframe_pad = cls.getPaddingChars(decimal_places, pad_style=pad_style)
     else:
         seq._subframe_pad = ''
     finish_new_seq(seq)
     return seq
Example #13
0
    def setFrameSet(self, frameSet):
        """
        Set a new :class:`.FrameSet` for the sequence.

        Args:
            frameSet (:class:`.FrameSet`): the new :class:`.FrameSet` object
        """
        if frameSet is not None and frameSet.hasSubFrames():
            if all(isinstance(frame, decimal.Decimal) for frame in frameSet):
                frameSet = FrameSet([
                    utils.quantize(frame, self._decimal_places)
                    for frame in frameSet
                ])
        self._frameSet = frameSet
Example #14
0
    def yield_sequences_in_list(cls,
                                paths,
                                using=None,
                                pad_style=PAD_STYLE_DEFAULT,
                                allow_subframes=False):
        """
        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.

        A template :obj:`FileSequence` object can also be provided via the
        ``using`` parameter. Given this template, the dirname, basename, and
        extension values will be used to extract the frame value from the paths
        instead of parsing each path from scratch.

        Examples:
            The ``using`` field can supply a template for extracting the frame
            component from the paths::

                paths = [
                    '/dir/file_001.0001.ext',
                    '/dir/file_002.0001.ext',
                    '/dir/file_003.0001.ext',
                ]
                template = FileSequence('/dir/file_#.0001.ext')
                seqs = FileSequence.yield_sequences_in_list(paths, using)
                # [<FileSequence: '/dir/file_1-3@@@.0001.ext'>]

        Args:
            paths (list[str]): a list of paths
            using (:obj:`FileSequence`): Optional sequence to use as template
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style
            allow_subframes (bool): if True, handle subframe filenames

        Yields:
            :obj:`FileSequence`:
        """
        seqs = {}
        if allow_subframes:
            _check = cls.DISK_SUB_RE.match
        else:
            _check = cls.DISK_RE.match

        using_template = isinstance(using, FileSequence)

        if using_template:
            dirname, basename, ext = using.dirname(), using.basename(
            ), using.extension()
            head = len(dirname + basename)
            tail = -len(ext)
            frames = set()

            for path in filter(None, map(utils.asString, paths)):
                frame = path[head:tail]
                try:
                    int(frame)
                except ValueError:
                    if not allow_subframes:
                        continue
                    try:
                        decimal.Decimal(frame)
                    except decimal.DecimalException:
                        continue
                _, _, subframe = frame.partition(".")
                key = (dirname, basename, ext, len(subframe))
                seqs.setdefault(key, frames).add(frame)

        else:
            for match in filter(None, map(_check, map(utils.asString, paths))):
                dirname, basename, frame, ext = match.groups()
                if not basename and not ext:
                    continue
                if frame:
                    _, _, subframe = frame.partition(".")
                    key = (dirname, basename, ext, len(subframe))
                else:
                    key = (dirname, basename, ext, 0)
                seqs.setdefault(key, set())
                if frame:
                    seqs[key].add(frame)

        for (dirname, basename, ext,
             decimal_places), frames in iteritems(seqs):
            # build the FileSequence behind the scenes, rather than dupe work
            seq = cls.__new__(cls)
            seq._dir = dirname or ''
            seq._base = basename or ''
            seq._ext = ext or ''
            seq._pad_style = pad_style
            if frames:
                seq._frameSet = FrameSet(frames)

                frame_lengths = set()
                for frame in frames:
                    frame_num, _, _ = frame.partition(".")
                    frame_lengths.add(len(frame_num))

                seq._frame_pad = cls.getPaddingChars(min(frame_lengths),
                                                     pad_style=pad_style)
                if decimal_places:
                    seq._subframe_pad = cls.getPaddingChars(
                        decimal_places, pad_style=pad_style)
                else:
                    seq._subframe_pad = ''
            else:
                seq._frameSet = None
                seq._frame_pad = ''
                seq._subframe_pad = ''

            if seq._subframe_pad:
                seq._pad = '.'.join([seq._frame_pad, seq._subframe_pad])
            else:
                seq._pad = seq._frame_pad

            seq.__init__(utils.asString(seq))
            yield seq
Example #15
0
class FileSequence(object):
    """:class:`FileSequence` represents an ordered sequence of files.
    
    If the frame size exceeds :obj:`fileseq.constants.MAX_FRAME_SIZE`, 
    a ``MaxSizeException`` will be thrown.

    :type sequence: str
    :param sequence: (ie: dir/path.1-100#.ext)
    :raises: :class:`fileseq.exceptions.MaxSizeException`
    """
    def __init__(self, sequence):

        sequence = utils.asString(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:
            self.setDirname(self._dir)

        self._zfill = self.__class__.getPaddingNum(self._pad)

    def copy(self):
        """
        Create a deep copy of this sequence

        :return: :obj:`fileseq.FileSequence`
        """
        fs = self.__class__.__new__(self.__class__)
        fs.__dict__ = self.__dict__.copy()
        fs._frameSet = None
        if self._frameSet is not None:
            fs._frameSet = self._frameSet.copy()
        return fs

    def format(self, template="{basename}{range}{padding}{extension}"):
        """Return the file sequence as a formatted string according to
        the given template.

        Utilizes the python string format syntax.  Available keys include:
            * basename - the basename of the sequence.
            * extension - the file extension of the sequence.
            * start - the start frame.
            * end - the end frame.
            * length - the length of the frame range.
            * padding - the detecting amount of padding.
            * inverted - the inverted frame range. (returns "" if none)
            * dirname - the directory name.
        
        If asking for the inverted range value, and the new inverted range
        exceeded :obj:`fileseq.constants.MAX_FRAME_SIZE`, a ``MaxSizeException``
        will be raised.

        :type template: str
        :rtype: str
        :raises: :class:`fileseq.exceptions.MaxSizeException` 
        """
        # Potentially expensive if inverted range is large
        # and user never asked for it in template
        inverted = (self.invertedFrameRange() or "") if "{inverted}" in template else ""

        return template.format(
            basename=self.basename(),
            extension=self.extension(), start=self.start(),
            end=self.end(), length=len(self),
            padding=self.padding(),
            range=self.frameRange() or "",
            inverted=inverted,
            dirname=self.dirname())

    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

    def dirname(self):
        """
        Return the directory name of the sequence.

        :rtype: str
        """
        return self._dir

    def setDirname(self, dirname):
        """
        Set a new directory name for the sequence.

        :type dirname: str
        :param dirname: the new directory name
        :rtype: None
        """
        # Make sure the dirname always ends in
        # a path separator character
        sep = utils._getPathSep(dirname)
        if not dirname.endswith(sep):
            dirname += sep

        self._dir = utils.asString(dirname)

    def basename(self):
        """
        Return the basename of the sequence.

        :rtype: str
        """
        return self._base

    def setBasename(self, base):
        """
        Set a new basename for the sequence.

        :type base: str
        :param base: the new base name
        :rtype: None
        """
        self._base = utils.asString(base)

    def padding(self):
        """
        Return the the padding characters in the sequence.

        :rtype: str
        """
        return self._pad

    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)

    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

    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

    def extension(self):
        """
        Return the file extension of the sequence, including leading period.

        :rtype: str
        """
        return self._ext

    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 = utils.asString(ext)

    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)

    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)

    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)

    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.

        If new inverted range exceeded :obj:`fileseq.constants.MAX_FRAME_SIZE`, 
        a ``MaxSizeException`` will be raised.

        :rtype: str
        :raises: :class:`fileseq.exceptions.MaxSizeException`
        """
        if not self._frameSet:
            return ''
        return self._frameSet.invertedFrameRange(self._zfill)

    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()

    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()

    def zfill(self):
        """
        Returns the zfill depth (ie the number of zeroes to pad with).

        :rtype: int
        """
        return self._zfill

    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))

    def index(self, idx):
        """
        Return the path to the file at the given index.

        :type idx: int
        :param idx: the desired index
        :rtype: str
        """
        return self.__getitem__(idx)

    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)

    def __getitem__(self, idx):
        """
        Allows indexing and slicing into the underlying :class:`fileseq.FrameSet`.

        When indexing, a string filepath is returns for the frame.

        When slicing, a new :class:`FileSequence` is returned.
        Slicing outside the range of the sequence results in an
        IndexError

        :type idx: int or slice
        :param idx: the desired index
        :rtype: str or :obj:`FileSequence`
        """
        if not self._frameSet:
            return str(self)

        frames = self._frameSet[idx]

        if not hasattr(idx, 'start'):
            return self.frame(frames)

        fset = FrameSet(frames)
        if fset.is_null:
            raise IndexError("slice is out of range and returns no frames")

        fs = self.copy()
        fs.setFrameSet(fset)
        return fs

    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))

    def __repr__(self):
        try:
            return "<FileSequence: '%s'>" % str(self)
        except TypeError:
            return super(FileSequence, self).__repr__()

    def __eq__(self, other):
        return str(self) == str(other)

    def __ne__(self, other):
        return str(self) != str(other)

    @staticmethod
    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, imap(utils.asString, 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
    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))

    @classmethod
    def findSequencesOnDisk(cls, pattern, include_hidden=False, strictPadding=False):
        """
        Yield the sequences found in the given directory.
        
        Example::
            findSequencesOnDisk('/path/to/files')

        The pattern can also specify glob-like shell wildcards including the following:
            ?         - 1 wildcard character
            *         - 1 or more wildcard character
            {foo,bar} - either 'foo' or 'bar'
        
        Exact frame ranges are not considered, and padding characters are converted to
        wildcards (# or @)

        Example::
            findSequencesOnDisk('/path/to/files/image_stereo_{left,right}.#.jpg')
            findSequencesOnDisk('/path/to/files/imag?_*_{left,right}.@@@.jpg', strictPadding=True)
        
        :param pattern: directory to scan, or pattern to filter in directory
        :type include_hidden: bool
        :param include_hidden: if true, show .hidden files as well
        :type strictPadding: bool
        :param strictPadding: if True, ignore files with padding length different from pattern
        :rtype: list
        """
        # reserve some functions we're going to need quick access to
        _not_hidden = lambda f: not f.startswith('.')
        _match_pattern = None
        _filter_padding = None
        _join = os.path.join

        dirpath, filepat = pattern, None

        # Support the pattern defining a filter for the files
        # in the existing directory
        if not os.path.isdir(pattern):
            dirpath, filepat = os.path.split(pattern)

            if not os.path.isdir(dirpath):
                return []

            # Start building a regex for filtering files
            seq = cls(filepat)
            patt = seq.basename().replace('.', r'\.')
            if seq.padding():
                patt += '\d+'
            if seq.extension():
                patt += seq.extension()

            # Convert braces groups into regex capture groups
            view = bytearray(patt)
            matches = re.finditer(r'{(.*?)(?:,(.*?))*}', patt)
            for match in reversed(list(matches)):
                i, j = match.span()
                view[i:j] = '(%s)' % '|'.join([m.strip() for m in match.groups()])
            view = view.replace('*', '.*')
            view = view.replace('?', '.')
            view += '$'
            try:
                _match_pattern = re.compile(str(view)).match
            except re.error:
                msg = 'Invalid file pattern: {}'.format(filepat)
                raise FileSeqException(msg)

            if seq.padding() and strictPadding:
                _filter_padding = functools.partial(cls._filterByPaddingNum, num=seq.zfill())

        # Get just the immediate files under the dir.
        # Avoids testing the os.listdir() for files as
        # a second step.
        ret = next(os.walk(dirpath), None)
        files = ret[-1] if ret else []

        # collapse some generators to get us the files that match our regex
        if not include_hidden:
            files = ifilter(_not_hidden, files)

        # Filter by files that match the provided file pattern
        if _match_pattern:
            files = ifilter(_match_pattern, files)

        # Filter by files that match the frame padding in the file pattern
        if _filter_padding:
            # returns a generator
            files = _filter_padding(files)

        # Ensure our dirpath ends with a path separator, so
        # that we can control which sep is used during the 
        # os.path.join
        sep = utils._getPathSep(dirpath)
        if not dirpath.endswith(sep):
            dirpath += sep

        files = (_join(dirpath, f) for f in files)
        files = list(files)

        return list(FileSequence.yield_sequences_in_list(files))

    @classmethod
    def findSequenceOnDisk(cls, pattern, strictPadding=False):
        """
        Search for a specific sequence on disk.

        The padding characters used in the pattern are used to filter the 
        frame values of the files on disk (if strictPadding is True).

        :Example:
            # Find sequence matching basename and extension, and a wildcard for
            # any frame.
            # returns bar.1.exr bar.10.exr, bar.100.exr, bar.1000.exr, inclusive
            >>> findSequenceOnDisk("seq/bar@@@@.exr")

            # Find exactly 4-padded sequence, i.e. seq/bar1-100#.exr
            # returns only frames bar1000.exr through bar9999.exr
            >>> findSequenceOnDisk("seq/bar#.exr", strictPadding=True)

           
        :param pattern: the sequence pattern being searched for
        :rtype: str
        :param strictPadding: if True, ignore files with padding length different from pattern
        :rtype: bool
        :raises: :class:`fileseq.exceptions.FileSeqException` if no sequence is found on disk
        """
        seq = cls(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()
        pad = seq.padding()

        globbed = iglob(patt)
        if pad and strictPadding:
            globbed = cls._filterByPaddingNum(globbed, seq.zfill())

        matches = cls.yield_sequences_in_list(globbed)
        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))

    @classmethod
    def _filterByPaddingNum(cls, iterable, num):
        """
        Yield only path elements from iterable which have a frame
        padding that matches the given target padding number
        """
        _check = DISK_RE.match

        for item in iterable:
            # Add a filter for paths that don't match the frame
            # padding of a given number
            matches = _check(item)
            if matches:
                actualNum = len(matches.group(3) or '')
                if actualNum != num:
                    continue
            
            yield item

    @staticmethod
    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
    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())))
Example #16
0
class FileSequence(object):
    """:class:`FileSequence` represents an ordered sequence of files.

    If the frame size exceeds :obj:`fileseq.constants.MAX_FRAME_SIZE`,
    a ``MaxSizeException`` will be thrown.

    :type sequence: str
    :param sequence: (ie: dir/path.1-100#.ext)
    :raises: :class:`fileseq.exceptions.MaxSizeException`
    """
    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:
            sep = utils._getPathSep(sequence)
            if not self._dir.endswith(sep):
                self._dir += sep

        self._zfill = self.__class__.getPaddingNum(self._pad)

    def format(self, template="{basename}{range}{padding}{extension}"):
        """Return the file sequence as a formatted string according to
        the given template.

        Utilizes the python string format syntax.  Available keys include:
            * basename - the basename of the sequence.
            * extension - the file extension of the sequence.
            * start - the start frame.
            * end - the end frame.
            * length - the length of the frame range.
            * padding - the detecting amount of padding.
            * inverted - the inverted frame range. (returns "" if none)
            * dirname - the directory name.

        If asking for the inverted range value, and the new inverted range
        exceeded :obj:`fileseq.constants.MAX_FRAME_SIZE`, a ``MaxSizeException``
        will be raised.

        :type template: str
        :rtype: str
        :raises: :class:`fileseq.exceptions.MaxSizeException`
        """
        # Potentially expensive if inverted range is large
        # and user never asked for it in template
        inverted = (self.invertedFrameRange()
                    or "") if "{inverted}" in template else ""

        return template.format(basename=self.basename(),
                               extension=self.extension(),
                               start=self.start(),
                               end=self.end(),
                               length=len(self),
                               padding=self.padding(),
                               range=self.frameRange() or "",
                               inverted=inverted,
                               dirname=self.dirname())

    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

    def dirname(self):
        """
        Return the directory name of the sequence.

        :rtype: str
        """
        return self._dir

    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

    def basename(self):
        """
        Return the basename of the sequence.

        :rtype: str
        """
        return self._base

    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

    def padding(self):
        """
        Return the the padding characters in the sequence.

        :rtype: str
        """
        return self._pad

    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)

    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

    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

    def extension(self):
        """
        Return the file extension of the sequence, including leading period.

        :rtype: str
        """
        return self._ext

    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

    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)

    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)

    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)

    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.

        If new inverted range exceeded :obj:`fileseq.constants.MAX_FRAME_SIZE`,
        a ``MaxSizeException`` will be raised.

        :rtype: str
        :raises: :class:`fileseq.exceptions.MaxSizeException`
        """
        if not self._frameSet:
            return ''
        return self._frameSet.invertedFrameRange(self._zfill)

    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()

    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()

    def zfill(self):
        """
        Returns the zfill depth (ie the number of zeroes to pad with).

        :rtype: int
        """
        return self._zfill

    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))

    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])

    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)

    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)

    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
    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 filter(None, map(_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.items():
            # 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(map(int,
                                                 frames))) if frames else None
                seq._pad = FileSequence.getPaddingChars(min(map(len, frames)))
            else:
                seq._frameSet = None
                seq._pad = ''
            seq.__init__(str(seq))
            yield seq

    @staticmethod
    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))

    @classmethod
    def findSequencesOnDisk(cls,
                            pattern,
                            include_hidden=False,
                            strictPadding=False):
        """
        Yield the sequences found in the given directory.

        Example::
            findSequencesOnDisk('/path/to/files')

        The pattern can also specify glob-like shell wildcards including the following:
            ?         - 1 wildcard character
            *         - 1 or more wildcard character
            {foo,bar} - either 'foo' or 'bar'

        Exact frame ranges are not considered, and padding characters are converted to
        wildcards (# or @)

        Example::
            findSequencesOnDisk('/path/to/files/image_stereo_{left,right}.#.jpg')
            findSequencesOnDisk('/path/to/files/imag?_*_{left,right}.@@@.jpg', strictPadding=True)

        :param pattern: directory to scan, or pattern to filter in directory
        :type include_hidden: bool
        :param include_hidden: if true, show .hidden files as well
        :type strictPadding: bool
        :param strictPadding: if True, ignore files with padding length different from pattern
        :rtype: list
        """
        # reserve some functions we're going to need quick access to
        _not_hidden = lambda f: not f.startswith('.')
        _match_pattern = None
        _filter_padding = None
        _join = os.path.join

        dirpath, filepat = pattern, None

        # Support the pattern defining a filter for the files
        # in the existing directory
        if not os.path.isdir(pattern):
            dirpath, filepat = os.path.split(pattern)

            if not os.path.isdir(dirpath):
                return []

            # Start building a regex for filtering files
            seq = cls(filepat)
            patt = seq.basename().replace('.', r'\.')
            if seq.padding():
                patt += '\d+'
            if seq.extension():
                patt += seq.extension()

            # Convert braces groups into regex capture groups
            view = bytearray(patt)
            matches = re.finditer(r'{(.*?)(?:,(.*?))*}', patt)
            for match in reversed(list(matches)):
                i, j = match.span()
                view[i:j] = '(%s)' % '|'.join(
                    [m.strip() for m in match.groups()])
            view = view.replace('*', '.*')
            view = view.replace('?', '.')
            view += '$'
            print(view)
            try:
                _match_pattern = re.compile(str(view)).match
            except re.error:
                msg = 'Invalid file pattern: {}'.format(filepat)
                raise FileSeqException(msg)

            if seq.padding() and strictPadding:
                _filter_padding = functools.partial(cls._filterByPaddingNum,
                                                    num=seq.zfill())

        # Get just the immediate files under the dir.
        # Avoids testing the os.listdir() for files as
        # a second step.
        ret = next(os.walk(dirpath), None)
        files = ret[-1] if ret else []

        # collapse some generators to get us the files that match our regex
        if not include_hidden:
            files = filter(_not_hidden, files)

        # Filter by files that match the provided file pattern
        if _match_pattern:
            files = filter(_match_pattern, files)

        # Filter by files that match the frame padding in the file pattern
        if _filter_padding:
            # returns a generator
            files = _filter_padding(files)

        # Ensure our dirpath ends with a path separator, so
        # that we can control which sep is used during the
        # os.path.join
        sep = utils._getPathSep(dirpath)
        if not dirpath.endswith(sep):
            dirpath += sep

        files = (_join(dirpath, f) for f in files)
        files = list(files)

        return list(FileSequence.yield_sequences_in_list(files))

    @classmethod
    def findSequenceOnDisk(cls, pattern, strictPadding=False):
        """
        Search for a specific sequence on disk.

        The padding characters used in the pattern are used to filter the
        frame values of the files on disk (if strictPadding is True).

        :Example:
            # Find sequence matching basename and extension, and a wildcard for
            # any frame.
            # returns bar.1.exr bar.10.exr, bar.100.exr, bar.1000.exr, inclusive
            >>> findSequenceOnDisk("seq/bar@@@@.exr")

            # Find exactly 4-padded sequence, i.e. seq/bar1-100#.exr
            # returns only frames bar1000.exr through bar9999.exr
            >>> findSequenceOnDisk("seq/bar#.exr", strictPadding=True)


        :param pattern: the sequence pattern being searched for
        :rtype: str
        :param strictPadding: if True, ignore files with padding length different from pattern
        :rtype: bool
        :raises: :class:`fileseq.exceptions.FileSeqException` if no sequence is found on disk
        """
        seq = cls(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()
        pad = seq.padding()

        globbed = iglob(patt)
        if pad and strictPadding:
            globbed = cls._filterByPaddingNum(globbed, seq.zfill())

        matches = cls.yield_sequences_in_list(globbed)
        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))

    @classmethod
    def _filterByPaddingNum(cls, iterable, num):
        """
        Yield only path elements from iterable which have a frame
        padding that matches the given target padding number
        """
        _check = DISK_RE.match

        for item in iterable:
            # Add a filter for paths that don't match the frame
            # padding of a given number
            matches = _check(item)
            if matches:
                actualNum = len(matches.group(3))
                if actualNum != num:
                    continue

            yield item

    @staticmethod
    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
    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())))
Example #17
0
class FileSequence(object):
    """:class:`FileSequence` represents an ordered sequence of files.

        Args:
            sequence (str): (ie: dir/path.1-100#.ext)

        Returns:
            :class:`FileSequence`:

        Raises:
            :class:`fileseq.exceptions.MaxSizeException`: If frame size exceeds
            ``fileseq.constants.MAX_FRAME_SIZE``
    """
    DISK_RE = DISK_RE
    DISK_SUB_RE = DISK_SUB_RE
    PAD_MAP = PAD_MAP
    REVERSE_PAD_MAP = REVERSE_PAD_MAP
    SPLIT_RE = SPLIT_RE
    SPLIT_SUB_RE = SPLIT_SUB_RE

    def __init__(self,
                 sequence,
                 pad_style=PAD_STYLE_DEFAULT,
                 allow_subframes=False):
        """Init the class
        """
        sequence = utils.asString(sequence)

        if not hasattr(self, '_frameSet'):

            self._frameSet = None

            if allow_subframes:
                split_re = self.SPLIT_SUB_RE
                disk_re = self.DISK_SUB_RE
            else:
                split_re = self.SPLIT_RE
                disk_re = self.DISK_RE

            try:
                # the main case, padding characters in the path.1-100#.exr
                path, frames, self._pad, self._ext = split_re.split(
                    sequence, 1)
                self._frame_pad, _, self._subframe_pad = self._pad.partition(
                    '.')
                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 self.PAD_MAP:
                    if placeholder in sequence:
                        msg = "Failed to parse FileSequence: {!r}"
                        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 = ''
                        self._frame_pad = ''
                        self._subframe_pad = ''
                    elif not frames:
                        self._pad = ''
                        self._frame_pad = ''
                        self._subframe_pad = ''
                        self._frameSet = None
                    else:
                        self._frameSet = FrameSet(frames)
                        if self._frameSet:
                            frame_num, _, subframe_num = frames.partition('.')
                            self._frame_pad = self.getPaddingChars(
                                len(frame_num), pad_style=pad_style)
                            if subframe_num:
                                self._subframe_pad = self.getPaddingChars(
                                    len(subframe_num), pad_style=pad_style)
                                self._pad = '.'.join(
                                    [self._frame_pad, self._subframe_pad])
                            else:
                                self._pad = self._frame_pad
                                self._subframe_pad = ''
                        else:
                            self._pad = ''
                            self._frame_pad = ''
                            self._subframe_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 = ''
                    self._frame_pad = ''
                    self._subframe_pad = ''

        if self._dir:
            self.setDirname(self._dir)

        self._pad_style = pad_style
        self._zfill = self.getPaddingNum(self._frame_pad, pad_style=pad_style)
        self._decimal_places = self.getPaddingNum(self._subframe_pad,
                                                  pad_style=pad_style)

        # Round subframes to match sequence
        if self._frameSet is not None and self._frameSet.hasSubFrames():
            self._frameSet = FrameSet([
                utils.quantize(frame, self._decimal_places)
                for frame in self._frameSet
            ])

    def copy(self):
        """
        Create a deep copy of this sequence

        Returns:
            :class:`.FileSequence`:
        """
        fs = self.__class__.__new__(self.__class__)
        fs.__dict__ = self.__dict__.copy()
        fs._frameSet = None
        if self._frameSet is not None:
            fs._frameSet = self._frameSet.copy()
        return fs

    def format(self, template="{basename}{range}{padding}{extension}"):
        """Return the file sequence as a formatted string according to
        the given template.

        Utilizes the python string format syntax.  Available keys include:
            * basename - the basename of the sequence.
            * range - the range of the sequence
            * padding - the detecting amount of padding.
            * extension - the file extension of the sequence.
            * start - the start frame.
            * end - the end frame.
            * length - the length of the frame range.
            * inverted - the inverted frame range. (returns "" if none)
            * dirname - the directory name.

        If asking for the inverted range value, and the new inverted range
        exceeded ``fileseq.constants.MAX_FRAME_SIZE``, a ``MaxSizeException``
        will be raised.

        Args:
            template (str):

        Returns:
            str:

        Raises:
            :class:`fileseq.exceptions.MaxSizeException`: If frame size exceeds
            `fileseq.constants.MAX_FRAME_SIZE``
        """
        try:
            return self._format(template)
        except UnicodeEncodeError:
            return self._format(futils.text_type(template))

    def _format(self, template):
        # Potentially expensive if inverted range is large
        # and user never asked for it in template
        inverted = (self.invertedFrameRange()
                    or "") if "{inverted}" in template else ""

        return template.format(basename=self.basename(),
                               extension=self.extension(),
                               start=self.start(),
                               end=self.end(),
                               length=len(self),
                               padding=self.padding(),
                               range=self.frameRange() or "",
                               inverted=inverted,
                               dirname=self.dirname())

    def split(self):
        """
        Split the :class:`FileSequence` into contiguous pieces and return them
        as a list of :class:`FileSequence` instances.

        Returns:
            list[:class:`FileSequence`]:
        """
        result = []
        for frange in self.frameRange().split(","):
            result.append(
                self.__class__(''.join(
                    (self._dir, self._base, frange, self._pad, self._ext))))
        return result

    def dirname(self):
        """
        Return the directory name of the sequence.

        Returns:
            str:
        """
        return self._dir

    def setDirname(self, dirname):
        """
        Set a new directory name for the sequence.

        Args:
            dirname (str): the new directory name
        """
        # Make sure the dirname always ends in
        # a path separator character
        dirname = utils.asString(dirname)
        sep = utils._getPathSep(dirname)
        if not dirname.endswith(sep):
            dirname = futils.native_str(dirname) + sep

        self._dir = dirname

    def basename(self):
        """
        Return the basename of the sequence.

        Returns:
            str: sequence basename
        """
        return self._base

    def setBasename(self, base):
        """
        Set a new basename for the sequence.

        Args:
            base (str): the new base name
        """
        self._base = utils.asString(base)

    def padStyle(self):
        """
        Return the the padding style of the sequence.
        See fileseq.constants.PAD_STYLE_HASH1 and fileseq.constants.PAD_STYLE_HASH4

        Returns:
            (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style
        """
        return self._pad_style

    def setPadStyle(self, pad_style):
        """
        Set new padding style for the sequence.
        See fileseq.constants.PAD_STYLE_HASH1 and fileseq.constants.PAD_STYLE_HASH4

        Args:
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style to set
        """
        self._pad_style = pad_style
        self._frame_pad = self.getPaddingChars(self._zfill,
                                               pad_style=pad_style)
        if self._decimal_places:
            self._subframe_pad = self.getPaddingChars(
                self._decimal_places, pad_style=self._pad_style)
            self._pad = '.'.join([self._frame_pad, self._subframe_pad])
        else:
            self._pad = self._frame_pad
            self._subframe_pad = ''

    def padding(self):
        """
        Return the the padding characters in the sequence.

        Returns:
            str: sequence padding
        """
        return self._pad

    def setPadding(self, padding):
        """
        Set new padding characters for the sequence.
        i.e. "#" or "@@@" or '%04d', or an empty string to disable range formatting.

        Args:
            padding (str): sequence padding to set
        """
        self._pad = padding
        self._frame_pad, _, self._subframe_pad = self._pad.partition('.')
        self._zfill = self.getPaddingNum(self._frame_pad,
                                         pad_style=self._pad_style)
        self._decimal_places = self.getPaddingNum(self._subframe_pad,
                                                  pad_style=self._pad_style)

    def framePadding(self):
        """
        Return the the padding characters in the sequence.

        Returns:
            str: sequence padding
        """
        return self._frame_pad

    def setFramePadding(self, padding):
        """
        Set new padding characters for the frames of the sequence.
        i.e. "#" or "@@@" or '%04d', or an empty string to disable range formatting.

        Args:
            padding (str): sequence padding to set
        """
        self._frame_pad = padding
        if self._subframe_pad:
            self._pad = '.'.join([self._frame_pad, self._subframe_pad])
        else:
            self._pad = self._frame_pad
        self._zfill = self.getPaddingNum(self._frame_pad,
                                         pad_style=self._pad_style)

    def subframePadding(self):
        """
        Return the the padding characters for subframes in the sequence.

        Returns:
            str: sequence padding
        """
        return self._subframe_pad

    def setSubframePadding(self, padding):
        """
        Set new padding characters for the subframes in the sequence.
        i.e. "#" or "@@@", or an empty string to disable range
        formatting.

        Args:
            padding (str): sequence padding to set
        """
        self._subframe_pad = padding
        if self._subframe_pad:
            self._pad = '.'.join([self._frame_pad, self._subframe_pad])
        else:
            self._pad = self._frame_pad
        self._decimal_places = self.getPaddingNum(self._subframe_pad,
                                                  pad_style=self._pad_style)

    def frameSet(self):
        """
        Return the :class:`.FrameSet` of the sequence if specified,
        otherwise None.

        Returns:
            :class:`.FrameSet` or None:
        """
        return self._frameSet

    def setFrameSet(self, frameSet):
        """
        Set a new :class:`.FrameSet` for the sequence.

        Args:
            frameSet (:class:`.FrameSet`): the new :class:`.FrameSet` object
        """
        if frameSet is not None and frameSet.hasSubFrames():
            if all(isinstance(frame, decimal.Decimal) for frame in frameSet):
                frameSet = FrameSet([
                    utils.quantize(frame, self._decimal_places)
                    for frame in frameSet
                ])
        self._frameSet = frameSet

    def extension(self):
        """
        Return the file extension of the sequence, including leading period.

        Returns:
            str:
        """
        return self._ext

    def setExtension(self, ext):
        """
        Set a new file extension for the sequence.

        Note:
            A leading period will be added if none is provided.

        Args:
            ext (str): the new file extension
        """
        if ext[0] != ".":
            ext = "." + ext
        self._ext = utils.asString(ext)

    def setExtention(self, ext):
        """
        Deprecated: use :meth:`setExtension`.

        Args:
            ext (str):
        """
        import warnings
        msg = "the setExtention method is deprecated, please use setExtension"
        warnings.warn(msg)
        self.setExtension(ext)

    def frameRange(self):
        """
        Returns the string formatted frame range of the sequence.
        Will return an empty string if the sequence has no frame pattern.

        Returns:
            str:
        """
        if not self._frameSet:
            return ''
        return self._frameSet.frameRange(self._zfill, self._decimal_places)

    def setFrameRange(self, frange):
        """
        Set a new frame range for the sequence.

        Args:
            frange (str): a properly formatted frame range, as per :class:`.FrameSet`
        """
        self._frameSet = FrameSet(frange)

    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,
        or the frame range includes subframes.

        Returns:
            str:

        Raises:
            :class:`fileseq.exceptions.MaxSizeException`: If new inverted range
                exceeded ``fileseq.constants.MAX_FRAME_SIZE``
        """
        if not self._frameSet or self._frameSet.hasSubFrames():
            return ''
        return self._frameSet.invertedFrameRange(self._zfill)

    def start(self):
        """
        Returns the start frame of the sequence's :class:`.FrameSet`.
        Will return 0 if the sequence has no frame pattern.

        Returns:
            int:
        """
        if not self._frameSet:
            return 0
        return self._frameSet.start()

    def end(self):
        """
        Returns the end frame of the sequences :class:`.FrameSet`.
        Will return 0 if the sequence has no frame pattern.

        Returns:
            int:
        """
        if not self._frameSet:
            return 0
        return self._frameSet.end()

    def zfill(self):
        """
        Returns the zfill depth (ie the number of zeroes to pad with).

        Returns:
            int:
        """
        return self._zfill

    def decimalPlaces(self):
        """
        Returns the number of decimal places to output.

        Returns:
            int or None:
        """
        return self._decimal_places

    def frame(self, frame):
        """
        Return a path for the given frame in the sequence.  Numeric values or
        numeric strings are treated as a frame number and padding is applied,
        all other values are passed though.

        Examples:
            >>> seq = FileSequence('/foo/bar.1-10#.exr')
            >>> seq.frame(1)
            '/foo/bar.0001.exr'
            >>> seq.frame("#")
            '/foo/bar.#.exr'

        Args:
            frame (int, float, decimal.Decimal or str): the desired frame number
                or a char to pass through (ie. #)

        Returns:
            str:
        """
        if self._zfill == 0:
            # There may have been no placeholder for frame IDs in
            # the sequence, in which case we don't want to insert
            # a frame ID
            zframe = ""
        else:
            zframe = None
            if not isinstance(frame, futils.integer_types +
                              (float, decimal.Decimal)):
                try:
                    frame = int(frame)
                except ValueError:
                    try:
                        frame = decimal.Decimal(frame)
                    except decimal.DecimalException:
                        zframe = frame
            if zframe is None:
                zframe = utils.pad(frame, self._zfill, self._decimal_places)

        return futils.native_str("".join(
            (self._dir, self._base, zframe, self._ext)))

    def index(self, idx):
        """
        Return the path to the file at the given index.

        Args:
            idx (int): the desired index

        Returns:
            str:
        """
        return self.__getitem__(idx)

    def __setstate__(self, state):
        """
        Allows for de-serialization from a pickled :class:`FileSequence`.

        Args:
            state (dict): Pickle dictionary producted by default pickle implementation
        """
        for name, value in state.items():
            self.__dict__[name] = value
        self.__dict__.setdefault('_pad_style', PAD_STYLE_DEFAULT)
        self.__dict__.setdefault('_frame_pad', self._pad)
        self.__dict__.setdefault('_subframe_pad', '')
        self.__dict__.setdefault('_decimal_places', 0)

    def __iter__(self):
        """
        Allow iteration over the path or paths this :class:`FileSequence`
        represents.

        Yields:
            :class:`FileSequence`:
        """
        # 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 utils.asString(self)
            return

        for f in self._frameSet:
            yield self.frame(f)

    def __getitem__(self, idx):
        """
        Allows indexing and slicing into the underlying :class:`.FrameSet`

        When indexing, a string filepath is returns for the frame.

        When slicing, a new :class:`FileSequence` is returned.
        Slicing outside the range of the sequence results in an
        IndexError

        Args:
            idx (int or slice): the desired index

        Returns:
            str or :obj:`FileSequence`:

        Raises:
            :class:`IndexError`: If slice is outside the range of the sequence
        """
        if not self._frameSet:
            return futils.native_str(self)

        frames = self._frameSet[idx]

        if not hasattr(idx, 'start'):
            return self.frame(frames)

        fset = FrameSet(frames)
        if fset.is_null:
            raise IndexError("slice is out of range and returns no frames")

        fs = self.copy()
        fs.setFrameSet(fset)
        return fs

    def __len__(self):
        """
        The length (number of files) represented by this :class:`FileSequence`.

        Returns:
            int:
        """
        if not self._frameSet or not self._zfill:
            return 1
        return len(self._frameSet)

    def __str__(self):
        """
        String representation of this :class:`FileSequence`.

        Returns:
            str:
        """
        frameSet = utils.asString(self._frameSet or "")
        return "".join((self._dir, self._base, frameSet,
                        self._pad if frameSet else "", self._ext))

    def __repr__(self):
        try:
            return "<%s: %r>" % (self.__class__.__name__, str(self))
        except TypeError:
            return super(self.__class__, self).__repr__()

    def __eq__(self, other):
        return str(self) == str(other)

    def __ne__(self, other):
        return str(self) != str(other)

    def __hash__(self):
        # TODO: Technically we should be returning None,
        # as this class is mutable and cannot reliably be hashed.
        # Python2 allows it without this definition.
        # Python3 fails with TypeError: unhashable.
        # For now, preserving the hashing behaviour in py3.
        return id(self)

    @classmethod
    def yield_sequences_in_list(cls,
                                paths,
                                using=None,
                                pad_style=PAD_STYLE_DEFAULT,
                                allow_subframes=False):
        """
        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.

        A template :obj:`FileSequence` object can also be provided via the
        ``using`` parameter. Given this template, the dirname, basename, and
        extension values will be used to extract the frame value from the paths
        instead of parsing each path from scratch.

        Examples:
            The ``using`` field can supply a template for extracting the frame
            component from the paths::

                paths = [
                    '/dir/file_001.0001.ext',
                    '/dir/file_002.0001.ext',
                    '/dir/file_003.0001.ext',
                ]
                template = FileSequence('/dir/file_#.0001.ext')
                seqs = FileSequence.yield_sequences_in_list(paths, using)
                # [<FileSequence: '/dir/file_1-3@@@.0001.ext'>]

        Args:
            paths (list[str]): a list of paths
            using (:obj:`FileSequence`): Optional sequence to use as template
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style
            allow_subframes (bool): if True, handle subframe filenames

        Yields:
            :obj:`FileSequence`:
        """
        seqs = {}
        if allow_subframes:
            _check = cls.DISK_SUB_RE.match
        else:
            _check = cls.DISK_RE.match

        using_template = isinstance(using, FileSequence)

        if using_template:
            dirname, basename, ext = using.dirname(), using.basename(
            ), using.extension()
            head = len(dirname + basename)
            tail = -len(ext)
            frames = set()

            for path in filter(None, map(utils.asString, paths)):
                frame = path[head:tail]
                try:
                    int(frame)
                except ValueError:
                    if not allow_subframes:
                        continue
                    try:
                        decimal.Decimal(frame)
                    except decimal.DecimalException:
                        continue
                _, _, subframe = frame.partition(".")
                key = (dirname, basename, ext, len(subframe))
                seqs.setdefault(key, frames).add(frame)

        else:
            for match in filter(None, map(_check, map(utils.asString, paths))):
                dirname, basename, frame, ext = match.groups()
                if not basename and not ext:
                    continue
                if frame:
                    _, _, subframe = frame.partition(".")
                    key = (dirname, basename, ext, len(subframe))
                else:
                    key = (dirname, basename, ext, 0)
                seqs.setdefault(key, set())
                if frame:
                    seqs[key].add(frame)

        def start_new_seq():
            seq = cls.__new__(cls)
            seq._dir = dirname or ''
            seq._base = basename or ''
            seq._ext = ext or ''
            return seq

        def finish_new_seq(seq):
            if seq._subframe_pad:
                seq._pad = '.'.join([seq._frame_pad, seq._subframe_pad])
            else:
                seq._pad = seq._frame_pad

            seq.__init__(utils.asString(seq),
                         pad_style=pad_style,
                         allow_subframes=allow_subframes)

        def get_frame_width(frame_str):
            frame_num, _, _ = frame_str.partition(".")
            return len(frame_num)

        def get_frame_minwidth(frame_str):
            # find the smallest padding width for a frame string
            frame_num, _, _ = frame_str.partition(".")
            size = len(frame_num)
            num = int(frame_num)
            num_size = len(str(num))
            if size == num_size:
                return 1
            return size

        def frames_to_seq(frames, pad_length, decimal_places):
            seq = start_new_seq()
            seq._frameSet = FrameSet(sorted(
                decimal.Decimal(f) for f in frames))
            seq._frame_pad = cls.getPaddingChars(pad_length,
                                                 pad_style=pad_style)
            if decimal_places:
                seq._subframe_pad = cls.getPaddingChars(decimal_places,
                                                        pad_style=pad_style)
            else:
                seq._subframe_pad = ''
            finish_new_seq(seq)
            return seq

        for (dirname, basename, ext,
             decimal_places), frames in iteritems(seqs):
            # Short-circuit logic if we do not have multiple frames, since we
            # only need to build and return a single simple sequence
            if not frames:
                seq = start_new_seq()
                seq._frameSet = None
                seq._frame_pad = ''
                seq._subframe_pad = ''
                finish_new_seq(seq)
                yield seq
                continue

            # If we have multiple frames, then we need to check them for different
            # padding and possibly yield more than one sequence.

            # sort the frame list by their string padding width
            sorted_frames = sorted(((get_frame_width(f), f) for f in frames),
                                   key=operator.itemgetter(0))

            current_frames = []
            current_width = None

            for width, frame in sorted_frames:
                # initialize on first item
                if current_width is None:
                    current_width = width

                if width != current_width and get_frame_minwidth(
                        frame) > current_width:
                    # We have a new padding length.
                    # Commit the current sequence, and then start a new one.
                    yield frames_to_seq(current_frames, current_width,
                                        decimal_places)

                    # Start tracking the next group of frames using the new length
                    current_frames = [frame]
                    current_width = width
                    continue

                current_frames.append(frame)

            # Commit the remaining frames as a sequence
            if current_frames:
                yield frames_to_seq(current_frames, current_width,
                                    decimal_places)

    @classmethod
    def findSequencesInList(cls,
                            paths,
                            pad_style=PAD_STYLE_DEFAULT,
                            allow_subframes=False):
        """
        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.

        Args:
            paths (list[str]): a list of paths
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style
            allow_subframes (bool): if True, handle subframe filenames

        Returns:
            list:
        """
        return list(
            cls.yield_sequences_in_list(paths,
                                        pad_style=pad_style,
                                        allow_subframes=allow_subframes))

    @classmethod
    def findSequencesOnDisk(cls,
                            pattern,
                            include_hidden=False,
                            strictPadding=False,
                            pad_style=PAD_STYLE_DEFAULT,
                            allow_subframes=False):
        """
        Yield the sequences found in the given directory.

        Examples::

            FileSequence.findSequencesOnDisk('/path/to/files')

        The `pattern` can also specify glob-like shell wildcards including the following:
            * ``?``         - 1 wildcard character
            * ``*``         - 1 or more wildcard character
            * ``{foo,bar}`` - either 'foo' or 'bar'

        Exact frame ranges are not considered, and padding characters are converted to
        wildcards (``#`` or ``@``)

        Examples::

            FileSequence.findSequencesOnDisk('/path/to/files/image_stereo_{left,right}.#.jpg')
            FileSequence.findSequencesOnDisk('/path/to/files/imag?_*_{left,right}.@@@.jpg', strictPadding=True)

        Args:
            pattern (str): directory to scan, or pattern to filter in directory
            include_hidden (bool): if true, show .hidden files as well
            strictPadding (bool): if True, ignore files with padding length different from pattern
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style
            allow_subframes (bool): if True, handle subframe filenames

        Returns:
            list:
        """
        # reserve some functions we're going to need quick access to
        _not_hidden = lambda f: not f.startswith('.')
        _match_pattern = None
        _filter_padding = None
        _join = os.path.join

        seq = None
        dirpath = pattern

        # Support the pattern defining a filter for the files
        # in the existing directory
        if not os.path.isdir(pattern):
            dirpath, filepat = os.path.split(pattern)

            if not os.path.isdir(dirpath):
                return []

            # Start building a regex for filtering files
            seq = cls(filepat,
                      pad_style=pad_style,
                      allow_subframes=allow_subframes)
            patt = r'\A'
            patt += cls._globCharsToRegex(seq.basename())
            if seq.padding():
                patt += '('
                if seq.framePadding():
                    patt += r'\d+'
                    if seq.subframePadding():
                        patt += r'\.\d+'
                patt += ')'
            if seq.extension():
                patt += cls._globCharsToRegex(seq.extension())

            # Convert braces groups into regex capture groups
            matches = re.finditer(r'{(.*?)(?:,(.*?))*}', patt)
            for match in reversed(list(matches)):
                i, j = match.span()
                regex = '(?:%s)' % '|'.join(
                    [m.strip() for m in match.groups()])
                patt = "".join((patt[0:i], regex, patt[j:]))
            patt += r'\Z'
            try:
                _match_pattern = re.compile(patt).match
            except re.error:
                msg = 'Invalid file pattern: {!r}'.format(filepat)
                raise FileSeqException(msg)

            if seq.padding() and strictPadding:
                get_frame = lambda f: _match_pattern(f).group(1)
                _filter_padding = functools.partial(
                    cls._filterByPaddingNum,
                    zfill=seq.zfill(),
                    decimal_places=seq.decimalPlaces(),
                    get_frame=get_frame)

        # Get just the immediate files under the dir.
        # Avoids testing the os.listdir() for files as
        # a second step.
        ret = next(os.walk(dirpath), None)
        files = ret[-1] if ret else []

        # collapse some generators to get us the files that match our regex
        if not include_hidden:
            files = filter(_not_hidden, files)

        # Filter by files that match the provided file pattern
        if _match_pattern:
            files = filter(_match_pattern, files)

        # Filter by files that match the frame padding in the file pattern
        if _filter_padding:
            # returns a generator
            files = _filter_padding(files)

        # Ensure our dirpath ends with a path separator, so
        # that we can control which sep is used during the
        # os.path.join
        sep = utils._getPathSep(dirpath)
        if not dirpath.endswith(sep):
            dirpath += sep

        files = [_join(dirpath, f) for f in files]

        seqs = list(
            cls.yield_sequences_in_list(files,
                                        pad_style=pad_style,
                                        allow_subframes=allow_subframes))

        if _filter_padding and seq:
            frame_pad = cls.conformPadding(seq.framePadding(),
                                           pad_style=pad_style)
            subframe_pad = cls.conformPadding(seq.subframePadding(),
                                              pad_style=pad_style)
            # strict padding should preserve the original padding
            # characters in the found sequences.
            for s in seqs:
                s.setFramePadding(frame_pad)
                s.setSubframePadding(subframe_pad)

        return seqs

    @classmethod
    def findSequenceOnDisk(cls,
                           pattern,
                           strictPadding=False,
                           pad_style=PAD_STYLE_DEFAULT,
                           allow_subframes=False):
        """
        Search for a specific sequence on disk.

        The padding characters used in the `pattern` are used to filter the
        frame values of the files on disk (if `strictPadding` is True).

        Examples:
            Find sequence matching basename and extension, and a wildcard for
            any frame.
            returns bar.1.exr bar.10.exr, bar.100.exr, bar.1000.exr, inclusive

            ``FileSequence.findSequenceOnDisk("seq/bar@@@@.exr")``

            Find exactly 4-padded sequence, i.e. seq/bar1-100#.exr
            returns only frames bar1000.exr through bar9999.exr

            ``FileSequence.findSequenceOnDisk("seq/bar#.exr", strictPadding=True)``

        Args:
            pattern (str): the sequence pattern being searched for
            strictPadding (bool): if True, ignore files with padding length different from `pattern`
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style
            allow_subframes (bool): if True, handle subframe filenames

        Returns:
            str:

        Raises:
            :class:`.FileSeqException`: if no sequence is found on disk
        """
        seq = cls(pattern,
                  allow_subframes=allow_subframes,
                  pad_style=pad_style)

        if seq.frameRange() == '' and seq.padding() == '':
            if os.path.isfile(pattern):
                return seq

        patt = seq.format('{dirname}{basename}*{extension}')

        dirname = seq.dirname()
        basename = seq.basename()
        ext = seq.extension()
        pad = seq.padding()
        frame_pad = seq.framePadding()
        subframe_pad = seq.subframePadding()

        globbed = iglob(patt)
        if pad:
            patt = r'\A'
            if dirname:
                patt = r'.*[/\\]'
            patt += re.escape(basename) + '(.*)' + re.escape(ext) + r'\Z'
            get_frame = lambda f: re.match(patt, f).group(1)

            if strictPadding:
                globbed = cls._filterByPaddingNum(
                    globbed,
                    seq.zfill(),
                    decimal_places=seq.decimalPlaces(),
                    get_frame=get_frame)
                frame_pad = cls.conformPadding(frame_pad, pad_style=pad_style)
                subframe_pad = cls.conformPadding(subframe_pad,
                                                  pad_style=pad_style)
            else:
                globbed = cls._filterByPaddingNum(
                    globbed,
                    None,
                    decimal_places=seq.decimalPlaces(),
                    get_frame=get_frame)

        sequences = []
        allow_subframes = bool(seq.decimalPlaces())
        for match in cls.yield_sequences_in_list(
                globbed,
                using=seq,
                pad_style=pad_style,
                allow_subframes=allow_subframes):
            if match.basename() == basename and match.extension() == ext:
                if pad and strictPadding:
                    match.setFramePadding(frame_pad)
                    match.setSubframePadding(subframe_pad)
                sequences.append(match)

        if len(sequences) == 1:
            return sequences[0]
        elif not sequences:
            msg = 'no sequence found on disk matching {0}'
        else:
            msg = 'multiple sequences found on disk matching {0}'
        raise FileSeqException(msg.format(pattern))

    @staticmethod
    def _globCharsToRegex(filename):
        """
        Translate single character elements of a shell pattern to make suitable
        for a regular expression pattern

        Args:
            filename (str): filename containing shell pattern to convert

        Returns:
            str:
        """
        filename = filename.replace('.', r'\.')
        filename = filename.replace('*', '.*')
        filename = filename.replace('?', '.')
        return filename

    @classmethod
    def _filterByPaddingNum(cls,
                            iterable,
                            zfill,
                            decimal_places=0,
                            get_frame=None):
        """
        Yield only path elements from iterable which have a frame padding that
        matches the given target padding numbers. If zfill is None only the
        subframe length is matched against decimal places. If provided get_frame
        should be a callable taking one argument that will extract the frame
        number from a filename.

        Args:
            iterable (collections.Iterable):
            zfill (int or None):
            decimal_places (int):
            get_frame (callable):

        Yields:
            str:
        """
        if decimal_places == 0:
            _check = cls.DISK_RE.match
        else:
            _check = cls.DISK_SUB_RE.match

        for item in iterable:
            # Add a filter for paths that don't match the frame
            # padding of a given number
            matches = _check(item)
            if not matches:
                if zfill is None or zfill <= 0:
                    # Not a sequence pattern, but we were asked
                    # to match on a zero padding
                    yield item

                continue

            # Ensure DISK_RE matches before calling optional get_frame function
            frame = matches.group(3) or ''
            if frame and get_frame is not None:
                frame = get_frame(item) or ''

            if not frame:
                if zfill is None or zfill <= 0:
                    # No frame value was parsed, but we were asked
                    # to match on a zero padding
                    yield item
                continue

            # We have a frame number
            frame, _, subframe = frame.partition(".")
            if len(subframe) != decimal_places:
                continue

            if zfill is None:
                yield item
                continue

            if frame[0] == '0' or frame[:2] == '-0':
                if len(frame) == zfill:
                    # A frame leading with '0' is explicitly
                    # padded and can only be a match if its exactly
                    # the target padding number
                    yield item
                continue

            if len(frame) >= zfill:
                # A frame that does not lead with '0' can match
                # a padding width >= to the target padding number
                yield item
                continue

    @classmethod
    def getPaddingChars(cls, num, pad_style=PAD_STYLE_DEFAULT):
        """
        Given a particular amount of padding, return the proper padding characters.

        Args:
            num (int): required width of string with padding
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style

        Returns:
            str:
        """
        num = max(1, num)
        reverse_pad_map = cls.REVERSE_PAD_MAP[pad_style]

        # Find the widest padding character that can be used alone
        for width in sorted(reverse_pad_map, reverse=True):
            if num % width == 0:
                return reverse_pad_map[width] * (num // width)

        # Should never reach here as all styles should have an entry for width 1
        raise FileSeqException(
            'REVERSE_PAD_MAP missing pad character for width 1')

    @classmethod
    def getPaddingNum(cls, chars, pad_style=PAD_STYLE_DEFAULT):
        """
        Given a supported group of padding characters, return the amount of padding.

        Args:
            chars (str): a supported group of padding characters
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style

        Returns:
            int:

        Raises:
            ValueError: if unsupported padding character is detected
        """
        if not chars:
            return 0

        match = PRINTF_SYNTAX_PADDING_RE.match(
            chars) or HOUDINI_SYNTAX_PADDING_RE.match(chars)
        if match:
            paddingNumStr = match.group(1)
            paddingNum = int(paddingNumStr) if paddingNumStr else 1
            return max(paddingNum, 1)

        try:
            rval = 0
            for char in chars:
                rval += cls.PAD_MAP[char][pad_style]
            return rval
        except KeyError:
            msg = "Detected an unsupported padding character: \"{}\"."
            msg += " Supported padding characters: {} or printf syntax padding"
            msg += " %<int>d"
            raise ValueError(
                msg.format(char, utils.asString(list(cls.PAD_MAP))))

    @classmethod
    def conformPadding(cls, chars, pad_style=PAD_STYLE_DEFAULT):
        """
        Ensure alternate input padding formats are conformed
        to formats defined in PAD_MAP

        If chars is already a format defined in PAD_MAP, then
        it is returned unmodified.

        Example::
            '#'    -> '#'
            '@@@@' -> '@@@@'
            '%04d' -> '#'

        Args:
            chars (str): input padding chars
            pad_style (`.PAD_STYLE_DEFAULT` or `.PAD_STYLE_HASH1` or `.PAD_STYLE_HASH4`): padding style

        Returns:
            str: conformed padding chars

        Raises:
            ValueError: If chars contains invalid padding characters
        """
        pad = chars
        if pad and pad[0] not in cls.PAD_MAP:
            num = cls.getPaddingNum(pad, pad_style=pad_style)
            pad = cls.getPaddingChars(num, pad_style=pad_style)
        return pad
Example #18
0
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 = sum([PAD_MAP[c] for c in self._pad])

    def format(self, template="{basename}{range}{padding}{extension}"):
        """Return the file sequence as a formatted string according to
        the given template.

        Utilizes the python string format syntax.  Available keys include:
            * basename - the basename of the sequence.
            * extension - the file extension of the sequence.
            * start - the start frame.
            * end - the end frame.
            * length - the length of the frame range.
            * padding - the detecting amount of padding.
            * inverted - the inverted frame range. (returns "" if none)
            * dirname - the directory name.

        :type template: str
        :rtype: str
        """
        return template.format(basename=self.basename(),
                               extension=self.extension(),
                               start=self.start(),
                               end=self.end(),
                               length=len(self),
                               padding=self.padding(),
                               range=self.frameRange() or "",
                               inverted=self.invertedFrameRange() or "",
                               dirname=self.dirname())

    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

    def dirname(self):
        """
        Return the directory name of the sequence.

        :rtype: str
        """
        return self._dir

    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

    def basename(self):
        """
        Return the basename of the sequence.

        :rtype: str
        """
        return self._base

    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

    def padding(self):
        """
        Return the the padding characters in the sequence.

        :rtype: str
        """
        return self._pad

    def setPadding(self, padding):
        """
        Set new padding characters for the sequence.
        i.e. "#" or "@@@", or an empty string to disable range formatting.

        :type padding: str
        :rtype: None
        """
        self._pad = padding
        self._zfill = sum([PAD_MAP[c] for c in self._pad])

    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

    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

    def extension(self):
        """
        Return the file extension of the sequence, including leading period.

        :rtype: str
        """
        return self._ext

    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

    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)

    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)

    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)

    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)

    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()

    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()

    def zfill(self):
        """
        Returns the zfill depth (ie the number of zeroes to pad with).

        :rtype: int
        """
        return self._zfill

    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))

    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])

    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)

    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)

    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
    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
    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
    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
    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)
        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
    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
    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
        """
        try:
            return sum([PAD_MAP[char] for char in chars])
        except KeyError:
            msg = "Detected an unsupported padding character: \"{}\"."
            msg += " Supported padding characters: {}."
            raise ValueError(msg.format(char, str(PAD_MAP.keys())))
Example #19
0
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 = sum([PAD_MAP[c] for c in self._pad])

    def format(self, template="{basename}{range}{padding}{extension}"):
        """Return the file sequence as a formatted string according to
        the given template.

        Utilizes the python string format syntax.  Available keys include:
            * basename - the basename of the sequence.
            * extension - the file extension of the sequence.
            * start - the start frame.
            * end - the end frame.
            * length - the length of the frame range.
            * padding - the detecting amount of padding.
            * inverted - the inverted frame range. (returns "" if none)
            * dirname - the directory name.

        :type template: str
        :rtype: str
        """
        return template.format(
            basename=self.basename(),
            extension=self.extension(),
            start=self.start(),
            end=self.end(),
            length=len(self),
            padding=self.padding(),
            range=self.frameRange() or "",
            inverted=self.invertedFrameRange() or "",
            dirname=self.dirname(),
        )

    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

    def dirname(self):
        """
        Return the directory name of the sequence.

        :rtype: str
        """
        return self._dir

    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

    def basename(self):
        """
        Return the basename of the sequence.

        :rtype: str
        """
        return self._base

    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

    def padding(self):
        """
        Return the the padding characters in the sequence.

        :rtype: str
        """
        return self._pad

    def setPadding(self, padding):
        """
        Set new padding characters for the sequence.
        i.e. "#" or "@@@", or an empty string to disable range formatting.

        :type padding: str
        :rtype: None
        """
        self._pad = padding
        self._zfill = sum([PAD_MAP[c] for c in self._pad])

    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

    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

    def extension(self):
        """
        Return the file extension of the sequence, including leading period.

        :rtype: str
        """
        return self._ext

    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

    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)

    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)

    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)

    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)

    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()

    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()

    def zfill(self):
        """
        Returns the zfill depth (ie the number of zeroes to pad with).

        :rtype: int
        """
        return self._zfill

    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))

    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])

    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)

    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)

    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
    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
    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
    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
    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
    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
    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
        """
        try:
            return sum([PAD_MAP[char] for char in chars])
        except KeyError:
            msg = 'Detected an unsupported padding character: "{}".'
            msg += " Supported padding characters: {}."
            raise ValueError(msg.format(char, str(PAD_MAP.keys())))
Example #20
0
    def __init__(self,
                 sequence,
                 pad_style=PAD_STYLE_DEFAULT,
                 allow_subframes=False):
        """Init the class
        """
        sequence = utils.asString(sequence)

        if not hasattr(self, '_frameSet'):

            self._frameSet = None

            if allow_subframes:
                split_re = self.SPLIT_SUB_RE
                disk_re = self.DISK_SUB_RE
            else:
                split_re = self.SPLIT_RE
                disk_re = self.DISK_RE

            try:
                # the main case, padding characters in the path.1-100#.exr
                path, frames, self._pad, self._ext = split_re.split(
                    sequence, 1)
                self._frame_pad, _, self._subframe_pad = self._pad.partition(
                    '.')
                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 self.PAD_MAP:
                    if placeholder in sequence:
                        msg = "Failed to parse FileSequence: {!r}"
                        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 = ''
                        self._frame_pad = ''
                        self._subframe_pad = ''
                    elif not frames:
                        self._pad = ''
                        self._frame_pad = ''
                        self._subframe_pad = ''
                        self._frameSet = None
                    else:
                        self._frameSet = FrameSet(frames)
                        if self._frameSet:
                            frame_num, _, subframe_num = frames.partition('.')
                            self._frame_pad = self.getPaddingChars(
                                len(frame_num), pad_style=pad_style)
                            if subframe_num:
                                self._subframe_pad = self.getPaddingChars(
                                    len(subframe_num), pad_style=pad_style)
                                self._pad = '.'.join(
                                    [self._frame_pad, self._subframe_pad])
                            else:
                                self._pad = self._frame_pad
                                self._subframe_pad = ''
                        else:
                            self._pad = ''
                            self._frame_pad = ''
                            self._subframe_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 = ''
                    self._frame_pad = ''
                    self._subframe_pad = ''

        if self._dir:
            self.setDirname(self._dir)

        self._pad_style = pad_style
        self._zfill = self.getPaddingNum(self._frame_pad, pad_style=pad_style)
        self._decimal_places = self.getPaddingNum(self._subframe_pad,
                                                  pad_style=pad_style)

        # Round subframes to match sequence
        if self._frameSet is not None and self._frameSet.hasSubFrames():
            self._frameSet = FrameSet([
                utils.quantize(frame, self._decimal_places)
                for frame in self._frameSet
            ])
Example #21
0
class FileSequence(object):
    """:class:`FileSequence` represents an ordered sequence of files.

        Args:
            sequence (str): (ie: dir/path.1-100#.ext)

        Returns:
            :class:`FileSequence`:

        Raises:
            :class:`fileseq.exceptions.MaxSizeException`: If frame size exceeds
            :const:`fileseq.constants.MAX_FRAME_SIZE`
    """
    def __init__(self, sequence):
        """Init the class
        """
        sequence = utils.asString(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:
            self.setDirname(self._dir)

        self._zfill = self.__class__.getPaddingNum(self._pad)

    def copy(self):
        """
        Create a deep copy of this sequence

        Returns:
            :obj:`.FileSequence`:
        """
        fs = self.__class__.__new__(self.__class__)
        fs.__dict__ = self.__dict__.copy()
        fs._frameSet = None
        if self._frameSet is not None:
            fs._frameSet = self._frameSet.copy()
        return fs

    def format(self, template="{basename}{range}{padding}{extension}"):
        """Return the file sequence as a formatted string according to
        the given template.

        Utilizes the python string format syntax.  Available keys include:
            * basename - the basename of the sequence.
            * extension - the file extension of the sequence.
            * start - the start frame.
            * end - the end frame.
            * length - the length of the frame range.
            * padding - the detecting amount of padding.
            * inverted - the inverted frame range. (returns "" if none)
            * dirname - the directory name.

        If asking for the inverted range value, and the new inverted range
        exceeded :const:`fileseq.constants.MAX_FRAME_SIZE`, a ``MaxSizeException``
        will be raised.

        Args:
            template (str):

        Returns:
            str:

        Raises:
            :class:`fileseq.exceptions.MaxSizeException`: If frame size exceeds
            :const:`fileseq.constants.MAX_FRAME_SIZE`
        """
        # Potentially expensive if inverted range is large
        # and user never asked for it in template
        inverted = (self.invertedFrameRange()
                    or "") if "{inverted}" in template else ""

        return template.format(basename=self.basename(),
                               extension=self.extension(),
                               start=self.start(),
                               end=self.end(),
                               length=len(self),
                               padding=self.padding(),
                               range=self.frameRange() or "",
                               inverted=inverted,
                               dirname=self.dirname())

    def split(self):
        """
        Split the :class:`FileSequence` into contiguous pieces and return them
        as a list of :class:`FileSequence` instances.

        Returns:
            list[:class:`FileSequence`]:
        """
        result = []
        for frange in self.frameRange().split(","):
            result.append(
                FileSequence(''.join(
                    (self._dir, self._base, frange, self._pad, self._ext))))
        return result

    def dirname(self):
        """
        Return the directory name of the sequence.

        Returns:
            str:
        """
        return self._dir

    def setDirname(self, dirname):
        """
        Set a new directory name for the sequence.

        Args:
            dirname (str): the new directory name
        """
        # Make sure the dirname always ends in
        # a path separator character
        sep = utils._getPathSep(dirname)
        if not dirname.endswith(sep):
            dirname += sep

        self._dir = utils.asString(dirname)

    def basename(self):
        """
        Return the basename of the sequence.

        Returns:
            str: sequence basename
        """
        return self._base

    def setBasename(self, base):
        """
        Set a new basename for the sequence.

        Args:
            base (str): the new base name
        """
        self._base = utils.asString(base)

    def padding(self):
        """
        Return the the padding characters in the sequence.

        Returns:
            str: sequence padding
        """
        return self._pad

    def setPadding(self, padding):
        """
        Set new padding characters for the sequence.
        i.e. "#" or "@@@" or '%04d', or an empty string to disable range formatting.

        Args:
            padding (str): sequence padding to set
        """
        self._pad = padding
        self._zfill = self.__class__.getPaddingNum(self._pad)

    def frameSet(self):
        """
        Return the :class:`.FrameSet` of the sequence if specified,
        otherwise None.

        Returns:
            :class:`.FrameSet` or None:
        """
        return self._frameSet

    def setFrameSet(self, frameSet):
        """
        Set a new :class:`.FrameSet` for the sequence.

        Args:
            frameSet (:class:`.FrameSet`): the new :class:`.FrameSet` object
        """
        self._frameSet = frameSet

    def extension(self):
        """
        Return the file extension of the sequence, including leading period.

        Returns:
            str:
        """
        return self._ext

    def setExtension(self, ext):
        """
        Set a new file extension for the sequence.

        Note:
            A leading period will be added if none is provided.

        Args:
            ext (str): the new file extension
        """
        if ext[0] != ".":
            ext = "." + ext
        self._ext = utils.asString(ext)

    def setExtention(self, ext):
        """
        Deprecated: use :meth:`setExtension`.

        Args:
            ext (str):
        """
        import warnings
        msg = "the setExtention method is deprecated, please use setExtension"
        warnings.warn(msg)
        self.setExtension(ext)

    def frameRange(self):
        """
        Returns the string formatted frame range of the sequence.
        Will return an empty string if the sequence has no frame pattern.

        Returns:
            str:
        """
        if not self._frameSet:
            return ''
        return self._frameSet.frameRange(self._zfill)

    def setFrameRange(self, frange):
        """
        Set a new frame range for the sequence.

        Args:
            frange (str): a properly formatted frame range, as per :class:`.FrameSet`
        """
        self._frameSet = FrameSet(frange)

    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.

        Returns:
            str:

        Raises:
            :class:`fileseq.exceptions.MaxSizeException`: If new inverted range
                exceeded :const:`fileseq.constants.MAX_FRAME_SIZE`
        """
        if not self._frameSet:
            return ''
        return self._frameSet.invertedFrameRange(self._zfill)

    def start(self):
        """
        Returns the start frame of the sequence's :class:`.FrameSet`.
        Will return 0 if the sequence has no frame pattern.

        Returns:
            int:
        """
        if not self._frameSet:
            return 0
        return self._frameSet.start()

    def end(self):
        """
        Returns the end frame of the sequences :class:`.FrameSet`.
        Will return 0 if the sequence has no frame pattern.

        Returns:
            int:
        """
        if not self._frameSet:
            return 0
        return self._frameSet.end()

    def zfill(self):
        """
        Returns the zfill depth (ie the number of zeroes to pad with).

        Returns:
            int:
        """
        return self._zfill

    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.

        Examples:
            >>> seq.frame(1)
            /foo/bar.0001.exr
            >>> seq.frame("#")
            /foo/bar.#.exr

        Args:
            frame (int or str): the desired frame number or a char to pass
                through (ie. #)

        Returns:
            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))

    def index(self, idx):
        """
        Return the path to the file at the given index.

        Args:
            idx (int): the desired index

        Returns:
            str:
        """
        return self.__getitem__(idx)

    def __iter__(self):
        """
        Allow iteration over the path or paths this :class:`FileSequence`
        represents.

        Yields:
            :class:`FileSequence`:
        """
        # 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)

    def __getitem__(self, idx):
        """
        Allows indexing and slicing into the underlying :class:`.FrameSet`

        When indexing, a string filepath is returns for the frame.

        When slicing, a new :class:`FileSequence` is returned.
        Slicing outside the range of the sequence results in an
        IndexError

        Args:
            idx (int or slice): the desired index

        Returns:
            str or :obj:`FileSequence`:

        Raises:
            :class:`IndexError`: If slice is outside the range of the sequence
        """
        if not self._frameSet:
            return str(self)

        frames = self._frameSet[idx]

        if not hasattr(idx, 'start'):
            return self.frame(frames)

        fset = FrameSet(frames)
        if fset.is_null:
            raise IndexError("slice is out of range and returns no frames")

        fs = self.copy()
        fs.setFrameSet(fset)
        return fs

    def __len__(self):
        """
        The length (number of files) represented by this :class:`FileSequence`.

        Returns:
            int:
        """
        if not self._frameSet or not self._zfill:
            return 1
        return len(self._frameSet)

    def __str__(self):
        """
        String representation of this :class:`FileSequence`.

        Returns:
            str:
        """
        frameSet = str(self._frameSet or "")
        return "".join((self._dir, self._base, frameSet,
                        self._pad if frameSet else "", self._ext))

    def __repr__(self):
        try:
            return "<FileSequence: '%s'>" % str(self)
        except TypeError:
            return super(FileSequence, self).__repr__()

    def __eq__(self, other):
        return str(self) == str(other)

    def __ne__(self, other):
        return str(self) != str(other)

    @staticmethod
    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.

        Args:
            paths (list[str]): a list of paths

        Yields:
            :obj:`FileSequence`:
        """
        seqs = {}
        _check = DISK_RE.match

        for match in ifilter(None, imap(_check, imap(utils.asString, 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
    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.

        Args:
            paths (list[str]): a list of paths

        Returns:
            list:
        """
        return list(FileSequence.yield_sequences_in_list(paths))

    @classmethod
    def findSequencesOnDisk(cls,
                            pattern,
                            include_hidden=False,
                            strictPadding=False):
        """
        Yield the sequences found in the given directory.

        Examples:
            >>> findSequencesOnDisk('/path/to/files')

        The `pattern` can also specify glob-like shell wildcards including the following:
            * ``?``         - 1 wildcard character
            * ``*``         - 1 or more wildcard character
            * ``{foo,bar}`` - either 'foo' or 'bar'

        Exact frame ranges are not considered, and padding characters are converted to
        wildcards (``#`` or ``@``)

        Examples:
            >>> findSequencesOnDisk('/path/to/files/image_stereo_{left,right}.#.jpg')
            >>> findSequencesOnDisk('/path/to/files/imag?_*_{left,right}.@@@.jpg', strictPadding=True)

        Args:
            pattern (str): directory to scan, or pattern to filter in directory
            include_hidden (bool): if true, show .hidden files as well
            strictPadding (bool): if True, ignore files with padding length different from pattern

        Returns:
            list:
        """
        # reserve some functions we're going to need quick access to
        _not_hidden = lambda f: not f.startswith('.')
        _match_pattern = None
        _filter_padding = None
        _join = os.path.join

        seq = None
        dirpath = pattern

        # Support the pattern defining a filter for the files
        # in the existing directory
        if not os.path.isdir(pattern):
            dirpath, filepat = os.path.split(pattern)

            if not os.path.isdir(dirpath):
                return []

            # Start building a regex for filtering files
            seq = cls(filepat)
            patt = seq.basename().replace('.', r'\.')
            if seq.padding():
                patt += '\d+'
            if seq.extension():
                patt += seq.extension()

            # Convert braces groups into regex capture groups
            view = bytearray(patt)
            matches = re.finditer(r'{(.*?)(?:,(.*?))*}', patt)
            for match in reversed(list(matches)):
                i, j = match.span()
                view[i:j] = '(%s)' % '|'.join(
                    [m.strip() for m in match.groups()])
            view = view.replace('*', '.*')
            view = view.replace('?', '.')
            view += '$'
            try:
                _match_pattern = re.compile(str(view)).match
            except re.error:
                msg = 'Invalid file pattern: {}'.format(filepat)
                raise FileSeqException(msg)

            if seq.padding() and strictPadding:
                _filter_padding = functools.partial(cls._filterByPaddingNum,
                                                    num=seq.zfill())

        # Get just the immediate files under the dir.
        # Avoids testing the os.listdir() for files as
        # a second step.
        ret = next(os.walk(dirpath), None)
        files = ret[-1] if ret else []

        # collapse some generators to get us the files that match our regex
        if not include_hidden:
            files = ifilter(_not_hidden, files)

        # Filter by files that match the provided file pattern
        if _match_pattern:
            files = ifilter(_match_pattern, files)

        # Filter by files that match the frame padding in the file pattern
        if _filter_padding:
            # returns a generator
            files = _filter_padding(files)

        # Ensure our dirpath ends with a path separator, so
        # that we can control which sep is used during the
        # os.path.join
        sep = utils._getPathSep(dirpath)
        if not dirpath.endswith(sep):
            dirpath += sep

        files = (_join(dirpath, f) for f in files)
        files = list(files)

        seqs = list(FileSequence.yield_sequences_in_list(files))

        if _filter_padding and seq:
            pad = cls.conformPadding(seq.padding())
            # strict padding should preserve the original padding
            # characters in the found sequences.
            for s in seqs:
                s.setPadding(pad)

        return seqs

    @classmethod
    def findSequenceOnDisk(cls, pattern, strictPadding=False):
        """
        Search for a specific sequence on disk.

        The padding characters used in the `pattern` are used to filter the
        frame values of the files on disk (if `strictPadding` is True).

        Examples:
            Find sequence matching basename and extension, and a wildcard for
            any frame.
            returns bar.1.exr bar.10.exr, bar.100.exr, bar.1000.exr, inclusive

            >>> findSequenceOnDisk("seq/bar@@@@.exr")

            Find exactly 4-padded sequence, i.e. seq/bar1-100#.exr
            returns only frames bar1000.exr through bar9999.exr

            >>> findSequenceOnDisk("seq/bar#.exr", strictPadding=True)

        Args:
            pattern (str): the sequence pattern being searched for
            strictPadding (bool): if True, ignore files with padding length different from `pattern`

        Returns:
            str:

        Raises:
            :class:`.FileSeqException`: if no sequence is found on disk
        """
        seq = cls(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()
        pad = seq.padding()

        globbed = iglob(patt)
        if pad and strictPadding:
            globbed = cls._filterByPaddingNum(globbed, seq.zfill())
            pad = cls.conformPadding(pad)

        matches = cls.yield_sequences_in_list(globbed)
        for match in matches:
            if match.basename() == basename and match.extension() == ext:
                if pad and strictPadding:
                    match.setPadding(pad)
                return match

        msg = 'no sequence found on disk matching {0}'
        raise FileSeqException(msg.format(pattern))

    @classmethod
    def _filterByPaddingNum(cls, iterable, num):
        """
        Yield only path elements from iterable which have a frame
        padding that matches the given target padding number

        Args:
            iterable (collections.Iterable):
            num (int):

        Yields:
            str:
        """
        _check = DISK_RE.match

        for item in iterable:
            # Add a filter for paths that don't match the frame
            # padding of a given number
            matches = _check(item)
            if not matches:
                if num <= 0:
                    # Not a sequence pattern, but we were asked
                    # to match on a zero padding
                    yield item

                continue

            frame = matches.group(3) or ''

            if not frame:
                if num <= 0:
                    # No frame value was parsed, but we were asked
                    # to match on a zero padding
                    yield item
                continue

            # We have a frame number

            if frame[0] == '0' or frame[:2] == '-0':
                if len(frame) == num:
                    # A frame leading with '0' is explicitly
                    # padded and can only be a match if its exactly
                    # the target padding number
                    yield item
                continue

            if len(frame) >= num:
                # A frame that does not lead with '0' can match
                # a padding width >= to the target padding number
                yield item
                continue

    @staticmethod
    def getPaddingChars(num):
        """
        Given a particular amount of padding, return the proper padding characters.

        Args:
            num (int):

        Returns:
            str:
        """
        if num == 0:
            return "@"
        if num % 4 == 0:
            return "#" * (num // 4)
        else:
            return "@" * num

    @staticmethod
    def getPaddingNum(chars):
        """
        Given a supported group of padding characters, return the amount of padding.

        Args:
            chars (str): a supported group of padding characters

        Returns:
            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())))

    @classmethod
    def conformPadding(cls, chars):
        """
        Ensure alternate input padding formats are conformed
        to formats defined in PAD_MAP

        If chars is already a format defined in PAD_MAP, then
        it is returned unmodified.

        Example::
            '#'    -> '#'
            '@@@@' -> '@@@@'
            '%04d' -> '#'

        Args:
            chars (str): input padding chars

        Returns:
            str: conformed padding chars

        Raises:
            ValueError: If chars contains invalid padding characters
        """
        pad = chars
        if pad and pad[0] not in PAD_MAP:
            pad = cls.getPaddingChars(cls.getPaddingNum(pad))
        return pad