Skip to content

Commit

Permalink
Add FileSequence (#1396)
Browse files Browse the repository at this point in the history
Link the Issue(s) this Pull Request is related to.
This fixes #242

* Added initial code for FileSequence together with tests imported from the old C implementation.

* Added docstrings and fixed linting

* Added ability to iterate and call specific frames

* Add support for len() and str() in FileSpec

* Re-enable layer output tests and change to Path instead of FileSpec

* Re-do callable function to allow for fileSequences without frameset to be callable and add test for it

* Added a getOpenRVPath function for easier being able to open in OpenRV.
  • Loading branch information
lithorus authored Jul 10, 2024
1 parent 0f8a717 commit ad89c30
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 4 deletions.
130 changes: 130 additions & 0 deletions pycue/FileSequence/FileSequence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Copyright Contributors to the OpenCue Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Helper class for representing a frame seqeunce path.
It supports a complex syntax implementing features such as comma-separated frame ranges,
stepped frame ranges and more. See the FileSequence class for more detail.
"""

import re
from .FrameSet import FrameSet


class FileSequence:
"""Represents a file path to a frame sequence"""
__filepath = None
frameSet = None
__basename = None
__prefix = None
__suffix = None
__dirname = ""
__padSize = 1
__iter_index = 0

def __init__(self, filepath):
"""
Construct a FileSequence object by parsing a filepath.
Details on how to specify a frame range can be seen in the FrameRange class
"""

filePathMatch = re.match(r"^(?P<pf>.*\.)(?P<fspec>[\d#,\-@x]+)(?P<sf>\.[.\w]+)$", filepath)
if filePathMatch is not None:
self.__filepath = filepath
self.__prefix = filePathMatch.group('pf')
self.__suffix = filePathMatch.group('sf')
dirmatch = re.match(r"^(?P<dirname>.*/)(?P<basename>.*)$", self.__prefix)
if dirmatch is not None:
self.__dirname = dirmatch.group("dirname")
self.__basename = dirmatch.group("basename")
else:
self.__basename = self.__prefix
framerangematch = re.match(r"^([\d\-x,]+)", filePathMatch.group("fspec"))
if framerangematch is not None:
self.frameSet = FrameSet(framerangematch.group(1))
if self.frameSet.get(0) > self.frameSet.get(-1):
raise ValueError('invalid filesequence range : ' + framerangematch.group(1))
firstFrameMatch = re.findall(r"^[-0]\d+", framerangematch.group(1))
if len(firstFrameMatch) > 0:
self.__padSize = len(firstFrameMatch[0])

padmatch = re.findall(r"#+$", filePathMatch.group("fspec"))
if len(padmatch) > 0:
self.__padSize = len(padmatch[0])
else:
raise ValueError('invalid filesequence path : ' + filepath)

def getPrefix(self):
"""Returns the prefix of the file sequence"""
return self.__prefix

def getSuffix(self):
"""Returns the suffix of the file sequence"""
return self.__suffix

def getDirname(self):
"""Returns the dirname of the file sequence, if given otherwise returns empty an string"""
return self.__dirname

def getBasename(self):
"""Returns the base name of the file sequence"""
return self.__basename.rstrip(".")

def getPadSize(self):
"""Returns the size of the frame padding. It defaults to 1 if none is detected"""
return self.__padSize

def getFileList(self, frameSet=None):
""" Returns the file list of the sequence """
filelist = []
paddingString = "%%0%dd" % self.getPadSize()
for frame in self.frameSet.getAll():
if frameSet is None or (isinstance(frameSet, FrameSet) and frame in frameSet.getAll()):
framepath = self.getPrefix() + paddingString % frame + self.getSuffix()
filelist.append(framepath)
return filelist

def getOpenRVPath(self, frameSet=None):
""" Returns a string specific for the OpenRV player"""
frameRange = ""
curFrameSet = frameSet or self.frameSet
if isinstance(curFrameSet, FrameSet):
frameRange = "%d-%d" % (curFrameSet.get(0), curFrameSet.get(-1))
framepath = self.getPrefix() + frameRange + "@"*self.__padSize + self.getSuffix()
return framepath

def __getitem__(self, index):
return self.getFileList()[index]

def __next__(self):
self.__iter_index += 1
if self.__iter_index <= len(self):
return self.getFileList()[self.__iter_index - 1]
raise StopIteration

def __iter__(self):
self.__iter_index = 0
return self

def __len__(self):
return len(self.getFileList())

def __call__(self, frame):
paddingString = "%%0%dd" % self.getPadSize()
framepath = self.getPrefix() + paddingString % frame + self.getSuffix()
return framepath

def __str__(self):
return self.__filepath
1 change: 1 addition & 0 deletions pycue/FileSequence/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@
from __future__ import division
from .FrameRange import FrameRange
from .FrameSet import FrameSet
from .FileSequence import FileSequence
110 changes: 110 additions & 0 deletions pycue/tests/file_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from FileSequence import FrameRange
from FileSequence import FrameSet
from FileSequence import FileSequence


class FrameRangeTests(unittest.TestCase):
Expand Down Expand Up @@ -189,5 +190,114 @@ def testNormalize(self):
self.assertEqual([1, 2, 3], duplicates.getAll())


class FileSequenceTests(unittest.TestCase):
def __testFileSequence(self, filespec, **kwargs):
fs = FileSequence(filespec)

tests = {'prefix': fs.getPrefix(),
'frameSet': fs.frameSet,
'suffix': fs.getSuffix(),
'padSize': fs.getPadSize(),
'dirname': fs.getDirname(),
'basename': fs.getBasename()
}

for arg, member in tests.items():
if arg in kwargs:
if isinstance(member, FrameSet):
self.assertEqual(member.getAll(), kwargs[arg].getAll(),
"Comparing '%s', got '%s', expected '%s'" % (arg, str(member),
str(kwargs[arg])))
else:
self.assertEqual(member, kwargs[arg],
"Comparing '%s', got '%s', expected '%s'" % (arg, str(member),
str(kwargs[arg])))

def testVariousFileSequences(self):
"""Test various file sequences are correctly parsed."""
self.__testFileSequence('foo.1-1####.bar', prefix='foo.', frameSet=FrameSet('1-1'),
suffix='.bar', padSize=4)
self.__testFileSequence('foo.####.bar', prefix='foo.', frameSet=None, suffix='.bar',
padSize=4)
# Not sure why this becomes padSize of 10
# self.__testFileSequence('foo.1-15x2#@#@.bar', prefix='foo.', frameSet=FrameSet('1-15x2'),
# suffix='.bar',
# padSize=10)
self.__testFileSequence('foo.1-15x2.bar', prefix='foo.', frameSet=FrameSet('1-15x2'),
suffix='.bar', padSize=1)
self.__testFileSequence('someImage.1,3,5####.rla', prefix='someImage.',
frameSet=FrameSet('1,3,5'), suffix='.rla', padSize=4)
self.__testFileSequence('foo.####.exr.tx', prefix='foo.', frameSet=None, suffix='.exr.tx',
padSize=4)
self.__testFileSequence('foo.1-10#.bar.1-9####.bar', prefix='foo.1-10#.bar.',
frameSet=FrameSet('1-9'), suffix='.bar', padSize=4)
self.__testFileSequence('foo.1-9.bar', prefix='foo.', frameSet=FrameSet('1-9'),
suffix='.bar', padSize=1)
self.__testFileSequence('foo.1-10.bar', prefix='foo.', frameSet=FrameSet('1-10'),
suffix='.bar', padSize=1)
self.__testFileSequence('foo.9.bar', prefix='foo.', frameSet=FrameSet('9-9'), suffix='.bar',
padSize=1)

self.__testFileSequence('foo.1-10#.bar', prefix='foo.', dirname='', basename='foo')
self.__testFileSequence('/foo.1-10#.bar', prefix='/foo.', dirname='/', basename='foo')
self.__testFileSequence('baz/foo.1-10#.bar', prefix='baz/foo.', dirname='baz/',
basename='foo')
self.__testFileSequence('/baz/foo.1-10#.bar', prefix='/baz/foo.', dirname='/baz/',
basename='foo')
self.__testFileSequence('/bar/baz/foo.1-10#.bar', prefix='/bar/baz/foo.',
dirname='/bar/baz/', basename='foo')

self.__testFileSequence('foo.-15-15####.bar', prefix='foo.', frameSet=FrameSet('-15-15'),
suffix='.bar', padSize=4)
self.__testFileSequence('foo.-15--1####.bar', prefix='foo.', frameSet=FrameSet('-15--1'),
suffix='.bar', padSize=4)

def testPadSizeWithoutPadTokens(self):
"""Test the pad size is correctly guessed when no padding tokens are given."""
self.__testFileSequence('foo.0009.bar', padSize=4)
self.__testFileSequence('foo.1-9x0002.bar', padSize=1)
# This test contradicts another test for negative steps
# self.__testFileSequence('foo.9-1x-0002.bar', padSize=1)
self.__testFileSequence('foo.9-09x0002.bar', padSize=1)
self.__testFileSequence('foo.9,10.bar', padSize=1)
self.__testFileSequence('foo.009,10.bar', padSize=3)
self.__testFileSequence('foo.-011.bar', padSize=4)

# sequence padded to 4 but frame count goes above 9999
self.__testFileSequence('foo.0001-10000.bar', padSize=4)

def testInvalidSequences(self):
"""Test invalid file sequences throw expected exception."""
self.assertRaises(ValueError, FileSequence, 'asdasdasda')
self.assertRaises(ValueError, FileSequence, 'foo.fred#.bar')
self.assertRaises(ValueError, FileSequence, 'foo..bar')
self.assertRaises(ValueError, FileSequence, 'foo.-,x#.bar')
self.assertRaises(ValueError, FileSequence, 'foo.x2.bar')
self.assertRaises(ValueError, FileSequence, 'foo.-20---10.bar')
# order reversed
self.assertRaises(ValueError, FileSequence, 'foo.10-1.bar')
self.assertRaises(ValueError, FileSequence, 'foo.-10--20.bar')
# require a prefix
self.assertRaises(ValueError, FileSequence, '.1')
self.assertRaises(ValueError, FileSequence, '0.1')

def __testStringify(self, filespec, index, expected):
fs = FileSequence(filespec)
self.assertEqual(expected, fs[index])

def testStringify(self):
self.__testStringify('foo.011.bar', 0, 'foo.011.bar')
self.__testStringify('foo.-011.bar', 0, 'foo.-011.bar')

def __testFrameList(self, filespec, frame, expected):
fs = FileSequence(filespec)
self.assertEqual(expected, fs(frame))

def testFrameList(self):
self.__testFrameList('foo.1-10.bar', 4, 'foo.4.bar')
self.__testFrameList('foo.1-10####.bar', 4, 'foo.0004.bar')
self.__testFrameList('foo.####.bar', 4, 'foo.0004.bar')


if __name__ == '__main__':
unittest.main()
6 changes: 2 additions & 4 deletions pyoutline/tests/layer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,9 +592,7 @@ class OutputRegistrationTest(unittest.TestCase):
def setUp(self):
outline.Outline.current = None

# TODO(bcipriano) Re-enable this test once FileSequence has a Python
# implementation. (Issue #242)
def disabled__test_output_passing(self):
def test_output_passing(self):
"""
Test that output registered in a pre-process is serialized
to a ol:outputs file in the render layer.
Expand All @@ -608,7 +606,7 @@ def disabled__test_output_passing(self):
# the preprocess
prelayer = outline.LayerPreProcess(layer1)
prelayer._execute = lambda frames: prelayer.add_output(
"test", outline.io.FileSpec("/tmp/foo.#.exr"))
"test", outline.io.Path("/tmp/foo.#.exr"))

# Add both to the outline
ol.add_layer(layer1)
Expand Down

0 comments on commit ad89c30

Please sign in to comment.