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))))
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 = utils.asString(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 = u"" return u"".join((self._dir, self._base, zframe, self._ext))
def isFrameRange(cls, frange): """ Return True if the given string is a frame range. Any padding characters, such as '#' and '@' are ignored. Args: frange (str): a frame range to test Returns: bool: """ # we're willing to trim padding characters from consideration # this translation is orders of magnitude faster than prior method if futils.PY2: frange = bytes(frange).translate(None, ''.join(cls.PAD_MAP.keys())) else: frange = str(frange) for key in cls.PAD_MAP: frange = frange.replace(key, '') if not frange: return True for part in asString(frange).split(','): if not part: continue try: cls._parse_frange_part(part) except ParseException: return False return True
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 utils.asString(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 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 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: rval = 0 for char in chars: rval += PAD_MAP[char] 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(PAD_MAP))))
def setBasename(self, base): """ Set a new basename for the sequence. Args: base (str): the new base name """ self._base = utils.asString(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 __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 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 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 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] != u".": ext = u"." + ext self._ext = utils.asString(ext)
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 __str__(self): """ String representation of this :class:`FileSequence`. Returns: str: """ frameSet = utils.asString(self._frameSet or u"") return u"".join(( self._dir, self._base, frameSet, self._pad if frameSet else u"", self._ext))
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 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 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 += sep self._dir = dirname
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)
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 __str__(self): """ String representation of this :class:`FileSequence`. Returns: str: """ frameSet = utils.asString(self._frameSet or "") parts = [ self._dir, self._base, frameSet, self._pad if frameSet else "", self._ext, ] if futils.PY2: for i, part in enumerate(parts): if isinstance(part, futils.text_type): parts[i] = futils.native(part.encode(utils.FILESYSTEM_ENCODING)) return "".join(parts)
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 filter(None, map(_check, map(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 iteritems(seqs): # build the FileSequence behind the scenes, rather than dupe work seq = FileSequence.__new__(FileSequence) seq._dir = dirname or u'' seq._base = basename or u'' seq._ext = ext or u'' 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 = u'' seq.__init__(utils.asString(seq)) yield seq
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
def __init__(self, frange): """Initialize the :class:`FrameSet` object. """ # if the user provides anything but a string, short-circuit the build if not isinstance(frange, futils.string_types): # if it's apparently a FrameSet already, short-circuit the build if set(dir(frange)).issuperset(self.__slots__): for attr in self.__slots__: setattr(self, attr, getattr(frange, attr)) return # if it's inherently disordered, sort and build elif isinstance(frange, Set): self._maxSizeCheck(frange) self._items = frozenset(normalizeFrames(frange)) self._order = tuple(sorted(self._items)) self._frange = self.framesToFrameRange( self._order, sort=False, compress=False) return # if it's ordered, find unique and build elif isinstance(frange, Sequence): self._maxSizeCheck(frange) items = set() order = unique(items, normalizeFrames(frange)) self._order = tuple(order) self._items = frozenset(items) self._frange = self.framesToFrameRange( self._order, sort=False, compress=False) return # if it's an individual number build directly elif isinstance(frange, futils.integer_types + (float, decimal.Decimal)): frame = normalizeFrame(frange) self._order = (frame, ) self._items = frozenset([frame]) self._frange = self.framesToFrameRange( self._order, sort=False, compress=False) # in all other cases, cast to a string else: try: frange = asString(frange) except Exception as err: msg = 'Could not parse "{0}": cast to string raised: {1}' raise ParseException(msg.format(frange, err)) # we're willing to trim padding characters from consideration # this translation is orders of magnitude faster than prior method if futils.PY2: frange = bytes(frange).translate(None, ''.join(self.PAD_MAP.keys())) self._frange = asString(frange) else: frange = str(frange) for key in self.PAD_MAP: frange = frange.replace(key, '') self._frange = asString(frange) # because we're acting like a set, we need to support the empty set if not self._frange: self._items = frozenset() self._order = tuple() return # build the mutable stores, then cast to immutable for storage items = set() order = [] maxSize = constants.MAX_FRAME_SIZE frange_parts = [] frange_types = [] for part in self._frange.split(","): # this is to deal with leading / trailing commas if not part: continue # parse the partial range start, end, modifier, chunk = self._parse_frange_part(part) frange_parts.append((start, end, modifier, chunk)) frange_types.extend(map(type, (start, end, chunk))) # Determine best type for numbers in range. Note that # _parse_frange_part will always return decimal.Decimal for subframes FrameType = int if decimal.Decimal in frange_types: FrameType = decimal.Decimal for start, end, modifier, chunk in frange_parts: # handle batched frames (1-100x5) if modifier == 'x': frames = xfrange(start, end, chunk, maxSize=maxSize) frames = [FrameType(f) for f in frames if f not in items] self._maxSizeCheck(len(frames) + len(items)) order.extend(frames) items.update(frames) # handle staggered frames (1-100:5) elif modifier == ':': if '.' in futils.native_str(chunk): raise ValueError("Unable to stagger subframes") for stagger in range(chunk, 0, -1): frames = xfrange(start, end, stagger, maxSize=maxSize) frames = [f for f in frames if f not in items] self._maxSizeCheck(len(frames) + len(items)) order.extend(frames) items.update(frames) # handle filled frames (1-100y5) elif modifier == 'y': if '.' in futils.native_str(chunk): raise ValueError("Unable to fill subframes") not_good = frozenset(xfrange(start, end, chunk, maxSize=maxSize)) frames = xfrange(start, end, 1, maxSize=maxSize) frames = (f for f in frames if f not in not_good) frames = [f for f in frames if f not in items] self._maxSizeCheck(len(frames) + len(items)) order.extend(frames) items.update(frames) # handle full ranges and single frames else: frames = xfrange(start, end, 1 if start < end else -1, maxSize=maxSize) frames = [FrameType(f) for f in frames if f not in items] self._maxSizeCheck(len(frames) + len(items)) order.extend(frames) items.update(frames) # lock the results into immutable internals # this allows for hashing and fast equality checking self._items = frozenset(items) self._order = tuple(order)
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 ])