Merge pull request #49 from BigRoy/master

Implement automatic context switch on Maya scene open and scene save
This commit is contained in:
Roy Nieterau 2017-11-15 10:28:02 +01:00 committed by GitHub
commit 684fc0cdf0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 246 additions and 3 deletions

View file

@ -1,6 +1,13 @@
import logging
from .vendor import pather
from .vendor.pather.error import ParseError
import avalon.io as io
import avalon.api
log = logging.getLogger(__name__)
def is_latest(representation):
"""Return whether the representation is from latest version
@ -43,4 +50,43 @@ def any_outdated():
return True
checked.add(representation)
return False
return False
def update_task_from_path(path):
"""Update the context using the current scene state.
When no changes to the context it will not trigger an update.
When the context for a file could not be parsed an error is logged but not
raised.
"""
if not path:
log.warning("Can't update the current task. Scene is not saved.")
return
# Find the current context from the filename
project = io.find_one({"type": "project"},
projection={"config.template.work": True})
template = project['config']['template']['work']
# Force to use the registered to root to avoid using wrong paths
template = pather.format(template, {"root": avalon.api.registered_root()})
try:
context = pather.parse(template, path)
except ParseError:
log.error("Can't update the current task. Unable to parse the "
"task for: %s", path)
return
# Find the changes between current Session and the path's context.
current = {
"asset": avalon.api.Session["AVALON_ASSET"],
"task": avalon.api.Session["AVALON_TASK"],
"app": avalon.api.Session["AVALON_APP"]
}
changes = {key: context[key] for key, current_value in current.items()
if context[key] != current_value}
if changes:
log.info("Updating work task to: %s", context)
avalon.api.update_current_task(**changes)

View file

@ -8,6 +8,10 @@ from maya import cmds
from avalon import api as avalon
from pyblish import api as pyblish
from ..lib import (
update_task_from_path,
any_outdated
)
from . import menu
from . import lib
@ -88,6 +92,9 @@ def on_save(_):
avalon.logger.info("Running callback on save..")
# Update current task for the current scene
update_task_from_path(cmds.file(query=True, sceneName=True))
# Generate ids of the current context on nodes in the scene
nodes = lib.get_id_required_nodes(referenced_nodes=False)
for node, new_id in lib.generate_ids(nodes):
@ -97,10 +104,12 @@ def on_save(_):
def on_open(_):
"""On scene open let's assume the containers have changed."""
from ..lib import any_outdated
from avalon.vendor.Qt import QtWidgets
from ..widgets import popup
# Update current task for the current scene
update_task_from_path(cmds.file(query=True, sceneName=True))
if any_outdated():
log.warning("Scene has outdated content.")
@ -124,4 +133,4 @@ def on_open(_):
dialog.setMessage("There are outdated containers in "
"your Maya scene.")
dialog.on_show.connect(_on_show_inventory)
dialog.show()
dialog.show()

0
colorbleed/vendor/__init__.py vendored Normal file
View file

5
colorbleed/vendor/pather/__init__.py vendored Normal file
View file

@ -0,0 +1,5 @@
__author__ = 'Roy Nieterau'
from .core import *
from .version import *

168
colorbleed/vendor/pather/core.py vendored Normal file
View file

@ -0,0 +1,168 @@
__all__ = ['parse', 'ls', 'ls_iter', 'format']
import os
import re
import string
import glob
from .error import ParseError
# Regex pattern that matches valid file
# TODO: Implement complete pattern if required
RE_FILENAME = '[-\w.,; \[\]]'
def format(pattern, data, allow_partial=True):
"""Format a pattern with a set of data
Examples:
Full formatting
>>> format("{a}/{b}/{c}", {"a": "foo", "b": "bar", "c": "nugget"})
'foo/bar/nugget'
Partial formatting
>>> format("{asset}/{character}", {"asset": "hero"})
'hero/{character}'
Disallow partial formatting
>>> format("{asset}/{character}", {"asset": "hero"},
... allow_partial=False)
Traceback (most recent call last):
...
KeyError: 'character'
Args:
pattern (str): The pattern to format.
data (dict): The key, value pairs used for formatting.
allow_partial (bool): Whether to raise error on partial format.
Returns:
str: The formatted result
"""
assert isinstance(data, dict)
if not all(isinstance(value, basestring) for value in data.values()):
raise TypeError("The values in the data "
"dictionary must be strings")
if allow_partial:
return _partial_format(pattern, data)
else:
return pattern.format(**data)
def parse(pattern, path):
"""Parse data from a path based on a pattern
Example:
>>> pattern = "root/{task}/{version}/data/"
>>> path = "root/modeling/v001/data/"
>>> parse(pattern, path)
{'task': 'modeling', 'version': 'v001'}
Returns:
dict: The data retrieved from path using pattern.
"""
pattern = os.path.normpath(pattern)
path = os.path.normpath(path)
# Force forward slashes
path = path.replace('\\', '/')
pattern = pattern.replace('\\', '/')
# Escape characters in path that are regex patterns so they are
# excluded by the regex searches. Exclude '{' and '}' in escaping.
pattern = re.escape(pattern)
pattern = pattern.replace('\{', '{').replace('\}', '}')
keys = re.findall(r'{(%s+)}' % RE_FILENAME,
pattern)
if not keys:
return []
# Find the corresponding values
value_pattern = re.sub(r'{(%s+)}' % RE_FILENAME,
r'(%s+)' % RE_FILENAME,
pattern)
match_values = re.match(value_pattern, path)
if not match_values:
raise ParseError("Path doesn't match with pattern. No values parsed")
values = match_values.groups()
return dict(zip(keys, values))
def ls_iter(pattern, include=None, with_matches=False):
"""Yield all matches for the given pattern.
If the pattern starts with a relative path (or a dynamic key) the search
will start from the current working directory, defined by os.path.realpath.
Arguments:
pattern (str): The pattern to match and search against.
include (dict): A dictionary used to target the search with the pattern
to include only those key-value pairs within the pattern. With this
you can reduce the filesystem query to a specified subset.
Example:
>>> data = {"root": "foobar", "content": "nugget"}
>>> for path in ls_iter("{root}/{project}/data/{content}/",
... include=data):
... print path
Returns:
(str, tuple): The matched paths (and data if `with_matches` is True)
The returned value changes whether `with_matches` parameter is True or
False. If True a 2-tuple is yielded for each match as (path, data) else
only the path is returned
"""
# format rule by data already provided to reduce query
if include is not None:
pattern = format(pattern, include, allow_partial=True)
pattern = os.path.expandvars(pattern)
pattern = os.path.realpath(pattern)
glob_pattern = re.sub(r'([/\\]{\w+}[/\\])', '/*/', pattern) # folder
glob_pattern = re.sub(r'({\w+})', '*', glob_pattern) # filename
for path in glob.iglob(glob_pattern):
path = os.path.realpath(path)
if with_matches:
data = parse(pattern, path)
yield path, data
else:
yield path
def ls(pattern, include=None, with_matches=False):
return list(ls_iter(pattern, include, with_matches=with_matches))
def _partial_format(s, data):
"""Return string `s` formatted by `data` allowing a partial format
Arguments:
s (str): The string that will be formatted
data (dict): The dictionary used to format with.
Example:
>>> _partial_format("{d} {a} {b} {c} {d}", {'b': "and", 'd': "left"})
'left {a} and {c} left'
"""
class FormatDict(dict):
def __missing__(self, key):
return "{" + key + "}"
formatter = string.Formatter()
mapping = FormatDict(**data)
return formatter.vformat(s, (), mapping)

5
colorbleed/vendor/pather/error.py vendored Normal file
View file

@ -0,0 +1,5 @@
class ParseError(ValueError):
"""Error raised when parsing a path with a pattern fails"""
pass

10
colorbleed/vendor/pather/version.py vendored Normal file
View file

@ -0,0 +1,10 @@
VERSION_MAJOR = 0
VERSION_MINOR = 1
VERSION_PATCH = 0
version_info = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
version = '%i.%i.%i' % version_info
__version__ = version
__all__ = ['version', 'version_info', '__version__']