287 lines
12 KiB
Python
287 lines
12 KiB
Python
# name_schema.py
|
|
"""
|
|
Object for managing schema directives from project YAML and applying them to parsing and mapping name fields.
|
|
"""
|
|
|
|
import string, collections
|
|
|
|
from .ranks import RankNotFound, Rank, Branch, Trunk
|
|
|
|
class FieldSchema(object):
|
|
"""
|
|
Represents a schema used for parsing and constructing a field in names.
|
|
|
|
We need naming information in various formats, based on knowledge about
|
|
the role of the Blender file and scene in the project. This object tracks
|
|
this information and returns correct names based on it via properties.
|
|
|
|
Note that FieldSchema is NOT an individual project unit name, but a defined
|
|
pattern for how names are treat at that level in the project. It is a class
|
|
of names, not a name itself. Thus "shot" has a schema, but is distinct from
|
|
"shot A" which is a particular "project unit". The job of the schema is
|
|
to tell us things like "shots in this project will be represented by
|
|
single capital letters".
|
|
|
|
See NameContext for the characteristics of a particular unit.
|
|
|
|
Attributes:
|
|
codetype (type): Type of code name used for this rank.
|
|
Usually it will be int, str, or Enum.
|
|
Pre-defined enumerations are available
|
|
for uppercase letters (_letters) and
|
|
lowercase letters (_lowercase) (Roman --
|
|
in principle, other alphabets could be added).
|
|
|
|
rank (Rank): Rank of hierarchy under project (which is 0). The
|
|
rank value increases as you go "down" the tree.
|
|
Sorry about that confusion.
|
|
|
|
|
|
ranks (Branch): List of named ranks known to schema (may include
|
|
both higher and lower ranks).
|
|
|
|
parent (|None):
|
|
Earlier rank to which this schema is attached.
|
|
|
|
format (str): Code for formatting with Python str.format() method.
|
|
Optional: format can also be specified with the
|
|
following settings, or left to default formatting.
|
|
|
|
pad (str): Padding character.
|
|
minlength (int): Minimum character length (0 means it may be empty).
|
|
maxlength (int): Maximum character length (0 means no limit).
|
|
|
|
words (bool): Treat name/title fields like a collection of words,
|
|
which can then be represented using "TitleCaps" or
|
|
"underscore_spacing", etc for identifier use.
|
|
|
|
delimiter (str): Field delimiter marking the end of this ranks'
|
|
code in designations. Note this is the delimiter
|
|
after this rank - the higher (lower value) rank
|
|
controls the delimiter used before it.
|
|
|
|
default: The default value for this rank. May be None,
|
|
in which case, the rank will be treated as unset
|
|
until a setting is made. The UI must provide a
|
|
means to restore the unset value. Having no values
|
|
set below a certain rank is how a NameContext's
|
|
rank is determined.
|
|
|
|
Note that the rank may go back to a lower value than the schema's
|
|
parent object in order to overwrite earlier schemas (for overriding a
|
|
particular branch in the project) or (compare this to the use of '..'
|
|
in operating system paths). Or it may skip a rank, indicating an
|
|
implied intermediate value, which will be treated as having a fixed
|
|
value. (I'm not certain I want that, but it would allow us to keep
|
|
rank numbers synchronized better in parallel hierarchies in a project).
|
|
|
|
Note that schemas can be overridden at any level in a project by
|
|
'project_schema' directives in unit YAML files, so it is possible to
|
|
change the schema behavior locally. By design, only lower levels in
|
|
the hierarchy (higher values of rank) can be affected by overrides.
|
|
|
|
This kind of use isn't fully developed yet, but the plan is to include
|
|
things like managing 'Library' assets with a very different structure
|
|
from shot files. This way, the project can split into 'Library' and
|
|
'Episode' forks with completely different schemas for each.
|
|
"""
|
|
# Defaults
|
|
_default_schema = {
|
|
'delimiter':'-',
|
|
|
|
'type': 'string',
|
|
'format':'{:s}',
|
|
'minlength':1, # Must be at least one character
|
|
'maxlength':0, # 0 means unlimited
|
|
'words': False, # If true, treat value as words and spaces
|
|
'pad': '0', # Left-padding character for fixed length
|
|
'default': None,
|
|
|
|
'rank': 'project',
|
|
'irank': 0,
|
|
'ranks': ('series', 'episode', 'sequence',
|
|
'block', 'camera', 'shot', 'element')
|
|
}
|
|
|
|
# Really this is more like a set than a dictionary right now, but I
|
|
# thought I might refactor to move the definitions into the dictionary:
|
|
_codetypes = {
|
|
'number':{},
|
|
'string':{},
|
|
'letter':{},
|
|
'lowercase':{},
|
|
}
|
|
|
|
_letter = tuple((A,A,A) for A in string.ascii_uppercase)
|
|
_lowercase = tuple((a,a,a) for a in string.ascii_lowercase)
|
|
|
|
rank = 'project'
|
|
irank = 0
|
|
default = None
|
|
|
|
#ranks = ('project',)
|
|
branch = Trunk
|
|
|
|
def __init__(self, parent=None, rank=None, schema=None, debug=False):
|
|
"""
|
|
Create a FieldSchema from schema data source.
|
|
|
|
FieldSchema is typically initialized based on data from YAML files
|
|
within the project. This allows us to avoid encoding project structure
|
|
into ABX, leaving how units are named up to the production designer.
|
|
|
|
If you want our suggestions, you can look at the "Lunatics!" project's
|
|
'lunatics.yaml' file, or the 'myproject.yaml' file in the ABX source
|
|
distribution.
|
|
|
|
Arguments:
|
|
parent (FieldSchema):
|
|
The level in the schema hierarchy above this one.
|
|
Should be None if this is the top.
|
|
|
|
rank (int): The rank of this schema to be created.
|
|
|
|
schema (dict): Data defining the schema, typically loaded from a
|
|
YAML file in the project.
|
|
|
|
debug (bool): Used only for testing. Turns on some verbose output
|
|
about internal implementation.
|
|
|
|
Note that the 'rank' is specified because it may NOT be sequential from
|
|
the parent schema.
|
|
"""
|
|
# Three types of schema data:
|
|
|
|
# Make sure schema is a copy -- no side effects!
|
|
if not schema:
|
|
schema = {}
|
|
else:
|
|
s = {}
|
|
s.update(schema)
|
|
schema = s
|
|
|
|
if not rank and 'rank' in schema:
|
|
rank = schema['rank']
|
|
|
|
# Stepped down in rank from parent:
|
|
self.parent = parent
|
|
|
|
if parent and rank:
|
|
# Check rank is defined in parent ranks and use that
|
|
# We can skip optional ranks
|
|
if rank in parent.ranks:
|
|
j = parent.ranks.index(rank)
|
|
self.ranks = parent.ranks[j+1:]
|
|
self.rank = rank
|
|
else:
|
|
# It's an error to ask for a rank that isn't defined
|
|
raise RankNotFound(
|
|
'"%s" not in defined ranks for "%s"' % (rank, parent))
|
|
|
|
elif parent and not rank:
|
|
# By default, get the first rank below parent
|
|
self.rank = parent.ranks[0]
|
|
self.ranks = parent.ranks[1:]
|
|
|
|
elif rank and not parent:
|
|
# With no parent, we're starting a new tree and renaming the root
|
|
self.rank = rank
|
|
self.ranks = self._default_schema['ranks']
|
|
|
|
else: # not rank and not parent:
|
|
# New tree with default rank
|
|
self.rank = self._default_schema['rank']
|
|
self.ranks = self._default_schema['ranks']
|
|
|
|
# Directly inherited/acquired from parent
|
|
# So far, only need a delimiter specified, but might be other stuff
|
|
self.delimiter = self._default_schema['delimiter']
|
|
if parent and parent.delimiter: self.delimiter = parent.delimiter
|
|
|
|
# Explicit override by the new schema:
|
|
if 'ranks' in schema: self.ranks = schema['ranks']
|
|
if 'delimiter' in schema: self.delimiter = schema['delimiter']
|
|
if 'default' in schema:
|
|
if schema['default'] == 'None':
|
|
self.default = None
|
|
else:
|
|
self.default = schema['default']
|
|
|
|
# Default unless specified (i.e. not inherited from parent)
|
|
newschema = {}
|
|
newschema.update(self._default_schema)
|
|
newschema.update(schema)
|
|
|
|
self.format = str(newschema['format'])
|
|
|
|
self.minlength = int(newschema['minlength'])
|
|
self.maxlength = int(newschema['maxlength'])
|
|
self.pad = str(newschema['pad'])
|
|
self.words = bool(newschema['words'])
|
|
|
|
if newschema['type'] == 'letter':
|
|
self.codetype = self._letter
|
|
|
|
elif newschema['type'] == 'lowercase':
|
|
self.codetype = self._lowercase
|
|
|
|
elif newschema['type'] == 'number':
|
|
# Recognized Python types
|
|
self.codetype = int
|
|
if 'minlength' or 'maxlength' in schema:
|
|
self.format = '{:0>%dd}' % self.minlength
|
|
|
|
elif newschema['type'] == 'string':
|
|
self.codetype = str
|
|
|
|
if ('minlength' in schema) or ('maxlength' in schema):
|
|
if self.maxlength == 0:
|
|
# Special case for unlimited length
|
|
self.format = '{:%1.1s>%ds}' % (self.pad, self.minlength)
|
|
self.format = '{:%1.1s>%d.%ds}' % (
|
|
self. pad, self.minlength, self.maxlength)
|
|
|
|
elif newschema['type'] == 'bool':
|
|
self.codetype = bool
|
|
|
|
elif isinstance(newschema['type'], collections.Sequence):
|
|
# Enumerated types
|
|
# This is somewhat specific to Blender -- setting the
|
|
# enumeration values requires a sequence in a particular format
|
|
self.codetype = []
|
|
for option in newschema['type']:
|
|
if type(option) is not str and isinstance(option, collections.Sequence):
|
|
option = tuple([str(e) for e in option][:3])
|
|
else:
|
|
option = (str(option), str(option), str(option))
|
|
self.codetype.append(option)
|
|
|
|
elif isinstance(newschema['type'], collections.Mapping):
|
|
self.codetype = []
|
|
for key, val in newschema['type'].items():
|
|
if type(val) is not str and isinstance(val, collections.Sequence):
|
|
if len(val) == 0:
|
|
option = (str(key), str(key), str(key))
|
|
elif len(val) == 1:
|
|
option = (str(key), str(val[0]), str(val[0]))
|
|
else:
|
|
option = (str(key), str(val[0]), str(val[1]))
|
|
else:
|
|
option = (str(key), str(val), str(val))
|
|
self.codetype.append(option)
|
|
else:
|
|
# If all else fails
|
|
self.codetype = None
|
|
|
|
def __repr__(self):
|
|
return('<(%s).FieldSchema: %s (%s, %s, %s, (%s))>' % (
|
|
repr(self.parent),
|
|
#self.irank,
|
|
self.rank,
|
|
self.delimiter,
|
|
self.default,
|
|
self.format,
|
|
self.codetype
|
|
))
|
|
|