# name_context.py """ NameContext defines the context of a particular named asset. Examples include scenes in a Blender file, but could be applied to other things. Also a base class for FileContext which applies to the whole file. NameContext handles the unified needs of these types, and in particular, hosts the methods used to generate names. """ import os, re import yaml from .name_schema import FieldSchema class NameContext(object): """ Single naming context within the file (e.g. a Blender scene). NameContext defines the characteristics of any particular project unit (taxon) within the project. So, for example, it may represent the context of an "Episode" or a "Sequence" or a "Shot". Used in Blender, it will typically be used in two ways: one to represent the entire file (as the base class for FileContext) and one to represent a particular Blender scene within the file, which may represent a single shot, multiple shots with the same camera, or perhaps just an element of a compositing shot created from multiple Blender scenes. Examples of all three uses occur in "Lunatics!" episode 1: the "Press Conference" uses multicam workflow, with scenes for each camera with multiple shots selected from the timeline, using the VSE; most scenes are done 'single camera' on a shot-per-scene basis; but some shots use a 'Freestyle camera clipping' technique which puts the line render in a separate (but linked) scene, while the final shot in the episode combines three different Blender scenes in a 2D composite, to effect a smooth transition to the titles. Attributes: container (NameContext): The project unit that contains this one. One step up the tree, but not necessarily the next step up in rank (because there can be skipped ranks). schemas (list(FieldSchema)): The schema list as seen by this unit, taking into account any schema overrides. namepath_segment (list): List of namepath codes defined in this object, not including the container's namepath. (Implementation) omit_ranks dict(str:int): How many ranks to omit from the beginning in shortened names for specific uses. (Implementation). Probably a mistake this isn't in the FieldSchema instead. fields (dict): The field values used to initialize the NameContext, May include some details not defined in this attribution API, and it includes the raw state of the 'name', 'code', and 'title' fields, determining which are authoritative -- i.e. fields which aren't specified are left to 'float', being generated by the related ones. Thus, name implies title, or title implies name. You can have one or the other or both, but the other will be generated from the provided one if it isn't specified. (Implementation). code (str|int|Enum): Identification code for the unit (I replaced 'id' in earlier versions, because 'id' is a reserved word in Python for Python memory reference / pointer identity. (R/W Property). namepath (list): List of codes for project units above this one. (R/O Property, generated from namepath_segment and container). rank (int): Rank of this unit (same as in Schema). (R/W Property, may affect other attributes). name (str): Short name for the unit. (R/W Property, may affect title). title (str): Full title for the unit. (R/W Property, may affect name). designation (str): Full designation for the unit, including all the namepath elements, but no title. fullname (str): The full designation, plus the current unit name. shortname (str):Abbreviated designation, according to omit_ranks, with name. """ def __init__(self, container, fields=None, namepath_segment=(), ): self.clear() if container or fields or namepath_segment: self.update(container, fields, namepath_segment) def clear(self): self.fields = {} self.schemas = ['project'] self.rank = 0 self.code = 'untitled' self.container = None self.namepath_segment = [] def update(self, container=None, fields=None, namepath_segment=()): self.container = container if namepath_segment: self.namepath_segment = namepath_segment else: self.namepath_segment = [] try: self.schemas = self.container.schemas except AttributeError: self.schemas = [] try: self.omit_ranks = self.container.omit_ranks except AttributeError: self.omit_ranks = {} self.omit_ranks.update({ 'edit': 0, 'render': 1, 'filename': 1, 'scene': 3}) if fields: if isinstance(fields, dict): self.fields.update(fields) elif isinstance(fields, str): self.fields.update(yaml.safe_load(fields)) def update_fields(self, data): self.fields.update(data) def _load_schemas(self, schemas, start=0): """ Load schemas from a list of schema dictionaries. @schemas: list of dictionaries containing schema field data (see FieldSchema). The data will typically be extracted from YAML, and is expected to be a list of dictionaries, each of which defines fields understood by the FieldSchema class, to instantiate FieldSchema objects. The result is a linked chain of schemas from the top of the project tree down. @start: if a start value is given, the top of the existing schema chain is kept, and the provided schemas starts under the rank of the start level in the existing schema. This is what happens when the schema is locally overridden at some point in the hierarchy. """ self.schemas = self.schemas[:start] if self.schemas: last = self.schemas[-1] else: last = None for schema in schemas: self.schemas.append(FieldSchema(last, schema['rank'], schema=schema)) #last = self.schemas[-1] def _parse_words(self, wordtext): words = [] groups = re.split(r'[\W_]', wordtext) for group in groups: if len(group)>1: group = group[0].upper() + group[1:] words.extend(re.findall(r'[A-Z][a-z]*', group)) elif len(group)==1: words.append(group[0].upper()) else: continue return words def _cap_words(self, words): return ''.join(w.capitalize() for w in words) def _underlower_words(self, words): return '_'.join(w.lower() for w in words) def _undercap_words(self, words): return '_'.join(w.capitalize() for w in words) def _spacecap_words(self, words): return ' '.join(w.capitalize() for w in words) def _compress_name(self, name): return self._cap_words(self._parse_words(name)) @property def namepath(self): if self.container: return self.container.namepath + self.namepath_segment else: return self.namepath_segment @property def rank(self): if 'rank' in self.fields: return self.fields['rank'] else: return None @rank.setter def rank(self, rank): self.fields['rank'] = rank @property def name(self): if 'name' in self.fields: return self.fields['name'] elif 'title' in self.fields: return self._compress_name(self.fields['title']) # elif 'code' in self.fields: # return self.fields['code'] else: return '' @name.setter def name(self, name): self.fields['name'] = name @property def code(self): if self.rank: return self.fields[self.rank]['code'] else: return self.fields['code'] @code.setter def code(self, code): if self.rank: self.fields[self.rank] = {'code': code} else: self.fields['code'] = code @property def description(self): if 'description' in self.fields: return self.fields['description'] else: return '' @description.setter def description(self, description): self.fields['description'] = str(description) def _get_name_components(self): components = [] for code, schema in zip(self.namepath, self.schemas): if code is None: continue components.append(schema.format.format(code)) components.append(schema.delimiter) return components[:-1] @property def fullname(self): if self.name: return (self.designation + self.schemas[-1].delimiter + self._compress_name(self.name) ) else: return self.designation @property def designation(self): return ''.join(self._get_name_components()) @property def shortname(self): namebase = self.omit_ranks['filename']*2 return (''.join(self._get_name_components()[namebase:]) + self.schemas[-1].delimiter + self._compress_name(self.name)) def get_scene_name(self, suffix=''): """ Create a name for the current scene, based on namepath. Arguments: suffix (str): Optional suffix code used to improve identifications of scenes. """ namebase = self.omit_ranks['scene']*2 desig = ''.join(self._get_name_components()[namebase:]) if suffix: return desig + ' ' + suffix else: return desig def get_render_path(self, suffix='', framedigits=5, ext='png'): """ Create a render filepath, based on namepath and parameters. Arguments: suffix (str): Optional unique code (usually for render profile). framedigits (int): How many digits to reserve for frame number. ext (str): Filetype extension for the render. This is meant to be called by render_profile to combine namepath based name with parameters for the specific render, to uniquely idenfify movie or image-stream output. """ desig = ''.join(self._get_name_components()[self.omit_ranks['render']+1:]) if ext in ('avi', 'mov', 'mp4', 'mkv'): if suffix: path = os.path.join(self.render_root, suffix, desig + '-' + suffix + '.' + ext) else: path = os.path.join(self.render_root, ext.upper(), desig + '.' + ext) else: if suffix: path = os.path.join(self.render_root, suffix, desig, desig + '-' + suffix + '-f' + '#'*framedigits + '.' + ext) else: path = os.path.join(self.render_root, ext.upper(), desig, desig + '-f' + '#'*framedigits + '.' + ext) return path