From 828a777764e225c41cd6f128fa5d5e92a384284a Mon Sep 17 00:00:00 2001 From: Marcus Ottosson Date: Fri, 23 Sep 2016 16:18:23 +0000 Subject: [PATCH] Working version, including containerisation --- README.md | 50 +- patch | 1171 ----------------- pyblish_starter/__init__.py | 4 + pyblish_starter/lib.py | 2 +- pyblish_starter/maya/lib.py | 29 + pyblish_starter/maya/pipeline.py | 144 +- pyblish_starter/pipeline.py | 46 +- pyblish_starter/plugins/collect_instances.py | 5 +- pyblish_starter/plugins/extract_animation.py | 12 +- pyblish_starter/plugins/extract_model.py | 18 +- pyblish_starter/plugins/extract_rig.py | 13 +- pyblish_starter/plugins/integrate_asset.py | 45 +- .../plugins/validate_model_hierarchy.py | 20 + pyblish_starter/plugins/validate_resources.py | 2 +- .../plugins/validate_rig_hierarchy.py | 20 + .../plugins/validate_rig_members.py | 7 +- .../plugins/validate_single_assembly.py | 26 + pyblish_starter/tests/test_pipeline.py | 22 +- 18 files changed, 338 insertions(+), 1298 deletions(-) delete mode 100644 patch create mode 100644 pyblish_starter/plugins/validate_model_hierarchy.py create mode 100644 pyblish_starter/plugins/validate_rig_hierarchy.py create mode 100644 pyblish_starter/plugins/validate_single_assembly.py diff --git a/README.md b/README.md index cce347e..7e7f68a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ### Pyblish Starter -A basic asset creation pipeline, with batteries included. +A basic asset creation pipeline - batteries included. > WARNING: Not ready for use. @@ -48,6 +48,12 @@ From here, you model, rig and animate as per the contract below. Build your own asset creation pipeline, starting with the basics. +**At a glance** + +- Categorise nodes within your workfile as being part of a single asset. +- Enable an asset library to identify published data on disk. +- Track when, where and from whom each asset come from. + **Overview** Asset creation covers all aspects related to building the assets used in the production of film. A film is typically partitioned into sequences and shots, where each shot consists of one or more assets. @@ -69,6 +75,15 @@ It includes a series of graphical user interfaces to aid the user in conforming

+### Batteries + +In addition to Pyblish cooperative plug-ins, a series of template workflow utilities are included. + +... + +
+
+ ### Terminology Starter reserves the following words for private and public use. Public members are exposed to the user, private ones are internal to the implementation. @@ -80,8 +95,35 @@ Starter reserves the following words for private and public use. Public members | ![][ver] | `version` | `X` | An asset iteration | v1, v034 | ![][rep] | `representation` | | A data format | Maya file, pointcache, thumbnail | ![][for] | `format` | | A file extension | `.ma`, `.abc`, `.ico`, `.png` -| ![][for] | `public` | | Shared data | v034 of Ryan -| ![][for] | `private` | | User data | Scenefile for v034 of Ryan +| ![][for] | `shared` | `X` | Public data | v034 of Ryan +| ![][for] | `user` | `X` | Private data | Scenefile for v034 of Ryan + +
+ +### Shared/user separation + +This project separates between data in progress, and data shared with others. + +Data in progress is any data in which a shared data is being produced. It is highly **mutable** and typically **private** to an individual artist. + +- **Mutable** implies + +Shared data on the other hand is **immutable**, **correct** and **impersonal**. + +- **Immutable** implies that the data may be dependent upon by other data. +- **Correct** implies passing validation of the associated family. +- **Impersonal** implies following strict organiasational conventions. + +### Ids + +... + +| Name | Description | Example +|:-----------------------------|:-------------------------|:---------- +| `pyblish.starter.container` | Incoming unit of data | `...:model_GRP`, `...:rig_GRP` +| `pyblish.starter.instance` | Outgoing unit of data | `Strange_model_default` + +
[ver]: https://cloud.githubusercontent.com/assets/2152766/18576835/f6b80574-7bdc-11e6-8237-1227f779815a.png [ast]: https://cloud.githubusercontent.com/assets/2152766/18576836/f6ca19e4-7bdc-11e6-9ef8-3614474c58bb.png @@ -140,6 +182,7 @@ A generic representation of geometry. ![req][] **Requirements** +- All DAG nodes must be parented to a single top-level transform - Static geometry (no deformers, generators) `*` - One shape per transform `*` - Zero transforms and pivots `*` @@ -177,6 +220,7 @@ The `starter.rig` contains the necessary implementation and interface for animat ![req][] **Requirements** +- All DAG nodes must be parented to a single top-level transform - Must contain an `objectSet` for controls and cachable geometry - Channels in `controls_SEL` at *default* values`*` - No input connection to animatable channel in `controls_SEL` `*` diff --git a/patch b/patch deleted file mode 100644 index 9f89752..0000000 --- a/patch +++ /dev/null @@ -1,1171 +0,0 @@ -From dad345e4021b5d0d51ae35904e479d2961e0329d Mon Sep 17 00:00:00 2001 -From: mottosso -Date: Thu, 22 Sep 2016 16:20:25 +0100 -Subject: [PATCH] Implement basic schema for ls() - ---- - .noserc | 5 - - pyblish_starter/__init__.py | 14 ++- - pyblish_starter/lib.py | 115 ++++++++++ - pyblish_starter/maya/__init__.py | 6 + - pyblish_starter/maya/lib.py | 129 ++++++++++-- - pyblish_starter/pipeline.py | 292 +++++++++++++++----------- - pyblish_starter/plugins/collect_instances.py | 5 +- - pyblish_starter/plugins/extract_animation.py | 1 + - pyblish_starter/plugins/extract_model.py | 1 + - pyblish_starter/plugins/extract_rig.py | 1 + - pyblish_starter/plugins/integrate_asset.py | 37 +++- - pyblish_starter/tests/test_pipeline.py | 134 +++++++++++- - pyblish_starter/tools/asset_loader/app.py | 57 +++++- - run_tests.py | 4 +- - setup.py | 2 +- - 15 files changed, 631 insertions(+), 172 deletions(-) - create mode 100644 pyblish_starter/lib.py - -diff --git a/.noserc b/.noserc -index 0f986f5..c509d78 100644 ---- a/.noserc -+++ b/.noserc -@@ -1,9 +1,4 @@ - [nosetests] - verbosity=2 - with-doctest=1 --with-coverage=1 - exclude=vendor --cover-html=1 --cover-erase=1 --cover-tests=1 --cover-package=pyblish_starter -\ No newline at end of file -diff --git a/pyblish_starter/__init__.py b/pyblish_starter/__init__.py -index 12f37cb..b4cf81e 100644 ---- a/pyblish_starter/__init__.py -+++ b/pyblish_starter/__init__.py -@@ -1,16 +1,28 @@ -+"""Public API -+ -+Anything that isn't defined here is INTERNAL and unreliable for external use. -+ -+""" -+ - from .pipeline import ( - ls, - install, -+ uninstall, - register_host, - register_plugins, -- format_private_dir, - ) - -+from .lib import ( -+ format_private_dir, -+ format_version, -+) - - __all__ = [ - "ls", - "install", -+ "uninstall", - "register_host", - "register_plugins", - "format_private_dir", -+ "format_version", - ] -diff --git a/pyblish_starter/lib.py b/pyblish_starter/lib.py -new file mode 100644 -index 0000000..31fba01 ---- /dev/null -+++ b/pyblish_starter/lib.py -@@ -0,0 +1,115 @@ -+import os -+import re -+import datetime -+ -+ -+def time(): -+ return datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%SZ") -+ -+ -+def format_private_dir(root, name): -+ dirname = os.path.join(root, "private", time(), name) -+ return dirname -+ -+ -+def parse_version(version): -+ """Return integer version from formatted string -+ -+ Example: -+ >>> parse_version("v001") -+ 1 -+ >>> parse_version("2") -+ 2 -+ >>> parse_version("version03") -+ 3 -+ >>> parse_version("000008") -+ 8 -+ >>> parse_version("abc") -+ Traceback (most recent call last): -+ ... -+ ValueError: Could not parse "abc" -+ -+ """ -+ -+ matches = re.findall(r"\d+", version) -+ -+ if not matches: -+ raise ValueError("Could not parse \"%s\"" % version) -+ -+ return int(matches[-1]) -+ -+ -+def format_version(version): -+ """Produce filesystem-friendly string from integer version -+ -+ Arguments: -+ version (int): Version number -+ -+ Returns: -+ string of `version`. -+ -+ Raises: -+ TypeError on non-integer version -+ -+ Example: -+ >>> format_version(5) -+ 'v005' -+ >>> format_version("x") -+ Traceback (most recent call last): -+ ... -+ TypeError: %d format: a number is required, not str -+ -+ """ -+ -+ return "v%03d" % version -+ -+ -+def find_latest_version(versions): -+ """Return latest version from list of versions -+ -+ If multiple numbers are found in a single version, -+ the last one found is used. E.g. (6) from "v7_22_6" -+ -+ Arguments: -+ versions (list): Version numbers as string -+ -+ Example: -+ >>> find_next_version(["v001", "v002", "v003"]) -+ 4 -+ >>> find_next_version(["1", "2", "3"]) -+ 4 -+ >>> find_next_version(["v1", "v0002", "verision_3"]) -+ 4 -+ >>> find_next_version(["v2", "5_version", "verision_8"]) -+ 9 -+ >>> find_next_version(["v2", "v3_5", "_1_2_3", "7, 4"]) -+ 6 -+ >>> find_next_version(["v010", "v011"]) -+ 12 -+ -+ """ -+ -+ highest_version = 0 -+ for version in versions: -+ version = parse_version(version) -+ -+ if version > highest_version: -+ highest_version = version -+ -+ return highest_version -+ -+ -+def find_next_version(versions): -+ """Return next version from list of versions -+ -+ See docstring for :func:`find_latest_version`. -+ -+ Arguments: -+ versions (list): Version numbers as string -+ -+ Returns: -+ int: Next version number -+ -+ """ -+ -+ return find_latest_version(versions) + 1 -diff --git a/pyblish_starter/maya/__init__.py b/pyblish_starter/maya/__init__.py -index ec672cf..f579201 100644 ---- a/pyblish_starter/maya/__init__.py -+++ b/pyblish_starter/maya/__init__.py -@@ -1,3 +1,9 @@ -+"""Public API -+ -+Anything that isn't defined here is INTERNAL and unreliable for external use. -+ -+""" -+ - from .lib import ( - install, - uninstall, -diff --git a/pyblish_starter/maya/lib.py b/pyblish_starter/maya/lib.py -index 64ceb68..e8c0411 100644 ---- a/pyblish_starter/maya/lib.py -+++ b/pyblish_starter/maya/lib.py -@@ -1,10 +1,11 @@ -+import os - import sys - import logging - - from maya import cmds, mel - - from .. import pipeline --from ..vendor.Qt import QtWidgets -+from ..vendor.Qt import QtWidgets, QtGui - - self = sys.modules[__name__] - self.log = logging.getLogger() -@@ -12,11 +13,20 @@ self.menu = "pyblishStarter" - - - def install(): -+ try: -+ import pyblish_maya -+ assert pyblish_maya.is_setup() -+ -+ except (ImportError, AssertionError): -+ _display_missing_dependencies() -+ - install_menu() -+ register_formats() - - - def uninstall(): - uninstall_menu() -+ deregister_formats() - - - def install_menu(): -@@ -24,14 +34,17 @@ def install_menu(): - - uninstall_menu() - -- cmds.menu(label="Pyblish Starter", tearOff=True, parent="MayaWindow") -+ cmds.menu(self.menu, -+ label="Pyblish Starter", -+ tearOff=True, -+ parent="MayaWindow") - cmds.menuItem("Create Instance", command=instance_creator.show) - cmds.menuItem("Load Asset", command=asset_loader.show) - - - def uninstall_menu(): -- widgets = {w.objectName(): w for w in QtWidgets.qApp.allWidgets()} -- menu = widgets.get("pyblishStarter") -+ widgets = dict((w.objectName(), w) for w in QtWidgets.qApp.allWidgets()) -+ menu = widgets.get(self.menu) - - if menu: - menu.deleteLater() -@@ -44,6 +57,18 @@ def root(): - cmds.workspace(directory=True, query=True)) - - -+def register_formats(): -+ pipeline.register_format(".ma") -+ pipeline.register_format(".mb") -+ pipeline.register_format(".abc") -+ -+ -+def deregister_formats(): -+ pipeline.deregister_format(".ma") -+ pipeline.deregister_format(".mb") -+ pipeline.deregister_format(".abc") -+ -+ - def hierarchy_from_string(hierarchy): - parents = {} - -@@ -83,33 +108,59 @@ def outmesh(shape, name=None): - outmesh = cmds.listRelatives(outmesh, parent=True)[0] - outmesh = cmds.rename(outmesh, name or "outMesh1") - cmds.sets(outmesh, addElement="initialShadingGroup") -+ - return outmesh - - --def loader(asset, version=-1, namespace=None): -+def loader(asset, version=-1): - """Load asset - -- The loader formats the `pipeline.root` variable with the -- following template members. -- -- - {project}: Absolute path to Maya project root. -- - Arguments: -- asset (str): Name of asset -+ asset (dict): Object of schema "pyblish-starter:asset-1.0" - version (int, optional): Version number, defaults to latest -- namespace (str, optional): Name of namespace -+ representation (str, optional): Representation to load, -+ defaults to "any" - - Returns: - Reference node - -+ Raises: -+ IndexError on version not found -+ ValueError on no supported representation -+ - """ - -+ assert asset["schema"] == "pyblish-starter:asset-1.0" - assert isinstance(version, int), "Version must be integer" - -- fname = pipeline.abspath(asset, version, ".ma").replace("\\", "/") -+ try: -+ version = asset["versions"][version] -+ except IndexError: -+ raise IndexError("\"%s\" of \"%s\" not found." % (version, asset)) -+ -+ formats = pipeline.registered_formats() -+ -+ # Pick any representation -+ representation = next( -+ (rep for rep in version["representations"] -+ if rep["format"] in formats), None -+ ) -+ -+ if representation is None: -+ raise ValueError( -+ "No supported representations for %s\n" -+ "Supported representations: %s" % ( -+ asset["name"], -+ ", ".join(r["format"] for r in version["representations"])) -+ ) -+ -+ fname = representation["path"].format( -+ asset=asset["path"], -+ version=pipeline.parse_version(version["version"]) -+ ) - - nodes = cmds.file(fname, -- namespace=namespace or ":", -+ namespace=asset["name"] + "_", - reference=True) - - return cmds.referenceQuery(nodes, referenceNode=True) -@@ -212,3 +263,53 @@ def export_alembic(nodes, file, frame_range=None, uv_write=True): - mel_cmd = "AbcExport -j \"{0}\"".format(mel_args_string) - - return mel.eval(mel_cmd) -+ -+ -+def _display_missing_dependencies(): -+ import pyblish -+ -+ messagebox = QtWidgets.QMessageBox() -+ messagebox.setIcon(messagebox.Warning) -+ messagebox.setWindowIcon(QtGui.QIcon(os.path.join( -+ os.path.dirname(pyblish.__file__), -+ "icons", -+ "logo-32x32.svg")) -+ ) -+ -+ spacer = QtWidgets.QWidget() -+ spacer.setMinimumSize(400, 0) -+ spacer.setSizePolicy(QtWidgets.QSizePolicy.Minimum, -+ QtWidgets.QSizePolicy.Expanding) -+ -+ layout = messagebox.layout() -+ layout.addWidget(spacer, layout.rowCount(), 0, 1, layout.columnCount()) -+ -+ messagebox.setWindowTitle("Uh oh") -+ messagebox.setText("Missing dependencies") -+ -+ messagebox.setInformativeText("""\ -+pyblish-starter requires pyblish-maya.\ -+""") -+ -+ messagebox.setDetailedText("""\ -+1) Install Pyblish for Maya -+ -+ $ pip install pyblish-maya -+ -+2) Run setup() -+ -+ >>> import pyblish_maya -+ >>> pyblish_maya.setup() -+ -+3) Try again. -+ -+ >>> pyblish_starter.install() -+ -+See https://github.com/pyblish/pyblish-starter for more information. -+""") -+ -+ messagebox.setStandardButtons(messagebox.Ok) -+ messagebox.exec_() -+ -+ raise RuntimeError("pyblish-starter requires pyblish-maya " -+ "to have been setup.") -diff --git a/pyblish_starter/pipeline.py b/pyblish_starter/pipeline.py -index 12ff3dd..5490e7d 100644 ---- a/pyblish_starter/pipeline.py -+++ b/pyblish_starter/pipeline.py -@@ -1,28 +1,43 @@ - import os --import re - import sys -+import json - import types - import logging --import datetime - - from pyblish import api - -+from . import lib -+ - self = sys.modules[__name__] - -+self.log = logging.getLogger() -+ - self._registered_data = list() - self._registered_families = list() -+self._registered_formats = list() -+ -+ -+def default_host(): -+ """A default host, in place of anything better -+ -+ This may be considered as reference for the -+ interface a host must implement. It also ensures -+ that the system runs, even when nothing is there -+ to support it. - --self._log = logging.getLogger() -+ """ -+ -+ host = types.ModuleType("default") -+ host.__dict__.update({ -+ "ls": lambda: ["Asset1", "Asset2"], -+ "loader": lambda asset, version, representation: None, -+ "creator": lambda name, family: "my_instance", -+ "supported_formats": lambda: [".ma", ".mb"] -+ }) - --# Mock host interface --host = types.ModuleType("default") --host.__dict__.update({ -- "ls": lambda: ["Asset1", "Asset2"], -- "loader": lambda asset, version, representation: None, -- "creator": lambda name, family: "my_instance" --}) -+ return host - --self._registered_host = host -+self._registered_host = default_host() - - - def install(host): -@@ -42,88 +57,151 @@ def install(host): - - register_host(host) - register_plugins() -+ register_default_data() -+ register_default_families() - -- register_data(key="id", value="pyblish.starter.instance") -- register_data(key="label", value="{name}") -- register_data(key="family", value="{family}") -- -- register_family( -- name="starter.model", -- help="Polygonal geometry for animation" -- ) - -- register_family( -- name="starter.rig", -- help="Character rig" -- ) -+def uninstall(): -+ try: -+ registered_host().uninstall() -+ except AttributeError: -+ pass - -- register_family( -- name="starter.animation", -- help="Pointcache" -- ) -+ deregister_host() -+ deregister_plugins() -+ deregister_default_data() -+ deregister_default_families() - - - def ls(): -- """List available assets""" -- root = self.registered_host().root() -- dirname = os.path.join(root, "public") -- self._log.debug("Listing %s" % dirname) -- -- try: -- return os.listdir(dirname) -- except OSError: -- return list() -+ """List available assets -+ -+ Return a list of available assets. -+ -+ Schema: -+ { -+ "schema": "pyblish-starter:asset-1.0", -+ "name": Name of directory, -+ "versions": [ -+ { -+ "version": 1, -+ "comment": "", -+ "representations": [ -+ { -+ "format": File extension, -+ "path": Filename -+ } -+ ] -+ }, -+ ] -+ } -+ -+ The interface of this function, along with its schema, is designed -+ to facilitate a potential transition into database-driven queries. -+ -+ A note on performance: -+ This function is a generator, it scans the system one asset -+ at a time. However, scanning implies both listing directories -+ and opening files - one per asset per version. -+ -+ Therefore, performance drops combinatorially for each new -+ version added to the project. -+ -+ In small pipelines - e.g. 100s of assets, with 10s of versions - -+ this should not pose a problem. -+ -+ In large pipelines - e.g. 1000s of assets, with 100s of versions - -+ this would likely become unbearable and manifest itself in -+ surrounding areas of the pipeline where disk-access is -+ critical; such as saving or loading files. -+ -+ ..note: The order of the list is undefined, but is typically alphabetical -+ due to how os.listdir() is implemented. -+ -+ ..note: The order of versions returned is guaranteed to be sorted, so -+ as to simplify retrieving the latest one via `versions[-1]` - -+ """ - --def abspath(asset, version=-1, representation=None): - root = registered_host().root() -+ assetsdir = os.path.join(root, "public") - -- dirname = os.path.join( -- root, -- "public", -- asset -- ) -+ for asset in os.listdir(assetsdir): -+ versionsdir = os.path.join(assetsdir, asset) - -- try: -- versions = os.listdir(dirname) -- except OSError: -- raise OSError("\"%s\" not found." % asset) -+ asset_entry = { -+ "schema": "pyblish-starter:asset-1.0", -+ "name": asset, -+ "versions": list() -+ } - -- # Automatically deduce version -- if version == -1: -- version = find_latest_version(versions) -+ for version in os.listdir(versionsdir): -+ versiondir = os.path.join(versionsdir, version) -+ fname = os.path.join(versiondir, ".metadata.json") - -- dirname = os.path.join( -- dirname, -- "v%03d" % version -- ) -+ try: -+ with open(fname) as f: -+ data = json.load(f) - -- try: -- representations = dict() -- for fname in os.listdir(dirname): -- name, ext = os.path.splitext(fname) -- representations[ext] = fname -+ except IOError: -+ self.log.warning("\"%s\" not found." % fname) -+ continue -+ -+ if data.get("schema") != "pyblish-starter:version-1.0": -+ self.log.warning("\"%s\" unsupported schema." % fname) -+ continue -+ -+ version_entry = { -+ "version": lib.parse_version(version), -+ "path": versiondir, -+ "representations": list() -+ } -+ -+ for representation in os.listdir(versiondir): -+ if representation.startswith("."): -+ continue -+ -+ name, ext = os.path.splitext(representation) -+ version_entry["representations"].append({ -+ "format": ext, -+ "path": "{dirname}/%s{format}" % name -+ }) -+ -+ asset_entry["versions"].append(version_entry) - -- if not representations: -- raise OSError -+ # Sort versions by integer -+ asset_entry["versions"].sort(key=lambda v: v["version"]) - -- except OSError: -- raise OSError("v%03d of \"%s\" not found." % (version, asset)) -+ yield asset_entry - -- # Automatically deduce representation -- if representation is None: -- fname = representations.values()[0] - -- return os.path.join( -- dirname, -- fname -+def register_default_data(): -+ register_data(key="id", value="pyblish.starter.instance") -+ register_data(key="label", value="{name}") -+ register_data(key="family", value="{family}") -+ -+ -+def register_default_families(): -+ register_family( -+ name="starter.model", -+ help="Polygonal geometry for animation" -+ ) -+ -+ register_family( -+ name="starter.rig", -+ help="Character rig" -+ ) -+ -+ register_family( -+ name="starter.animation", -+ help="Pointcache" - ) - - - def register_host(host): - for member in ("root", - "loader", -- "creator"): -+ "creator",): - assert hasattr(host, member), "Missing %s" % member - - self._registered_host = host -@@ -171,77 +249,39 @@ def register_family(name, data=None, help=None): - }) - - -+def registered_formats(): -+ return self._registered_formats[:] -+ -+ - def registered_families(): -- return list(self._registered_families) -+ return self._registered_families[:] - - - def registered_data(): -- return list(self._registered_data) -+ return self._registered_data[:] - - - def registered_host(): - return self._registered_host - - --def time(): -- return datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%SZ") -- -- --def format_private_dir(root, name): -- dirname = os.path.join(root, "private", time(), name) -- return dirname -- -- --def find_latest_version(versions): -- """Return latest version from list of versions -- -- If multiple numbers are found in a single version, -- the last one found is used. E.g. (6) from "v7_22_6" -- -- Arguments: -- versions (list): Version numbers as string -- -- Example: -- >>> find_next_version(["v001", "v002", "v003"]) -- 4 -- >>> find_next_version(["1", "2", "3"]) -- 4 -- >>> find_next_version(["v1", "v0002", "verision_3"]) -- 4 -- >>> find_next_version(["v2", "5_version", "verision_8"]) -- 9 -- >>> find_next_version(["v2", "v3_5", "_1_2_3", "7, 4"]) -- 6 -- >>> find_next_version(["v010", "v011"]) -- 12 -- -- """ -+def deregister_default_families(): -+ self._registered_families[:] = list() - -- highest_version = 0 -- for version in versions: -- matches = re.findall(r"\d+", version) - -- if not matches: -- continue -+def deregister_default_data(): -+ self._registered_data[:] = list() - -- version = int(matches[-1]) -- if version > highest_version: -- highest_version = version - -- return highest_version -- -- --def find_next_version(versions): -- """Return next version from list of versions -- -- See docstring for :func:`find_latest_version`. -- -- Arguments: -- versions (list): Version numbers as string -+def deregister_plugins(): -+ from . import plugins -+ plugin_path = os.path.dirname(plugins.__file__) - -- Returns: -- int: Next version number -+ try: -+ api.deregister_plugin_path(plugin_path) -+ except ValueError: -+ self.log.warning("pyblish-starter plug-ins not registered.") - -- """ - -- return find_latest_version(versions) + 1 -+def deregister_host(): -+ self._registered_host = default_host() -diff --git a/pyblish_starter/plugins/collect_instances.py b/pyblish_starter/plugins/collect_instances.py -index 1508bb2..e63e6c5 100644 ---- a/pyblish_starter/plugins/collect_instances.py -+++ b/pyblish_starter/plugins/collect_instances.py -@@ -44,12 +44,15 @@ class CollectStarterInstances(api.ContextPlugin): - from maya import cmds - - try: -+ # Assertion also made in pyblish_starter.install() -+ # but as plug-ins can be used vanilla, the check -+ # must also be made here. - import pyblish_maya - assert pyblish_maya.is_setup() - - except (ImportError, AssertionError): - raise RuntimeError("pyblish-starter requires pyblish-maya " -- "to have been install.") -+ "to have been setup.") - - for objset in cmds.ls("*.id", - long=True, # Produce full names -diff --git a/pyblish_starter/plugins/extract_animation.py b/pyblish_starter/plugins/extract_animation.py -index c232c9b..9dbc694 100644 ---- a/pyblish_starter/plugins/extract_animation.py -+++ b/pyblish_starter/plugins/extract_animation.py -@@ -48,5 +48,6 @@ class ExtractStarterAnimation(api.InstancePlugin): - - # Store reference for integration - instance.data["privateDir"] = dirname -+ instance.data["filename"] = filename - - self.log.info("Extracted {instance} to {dirname}".format(**locals())) -diff --git a/pyblish_starter/plugins/extract_model.py b/pyblish_starter/plugins/extract_model.py -index 704b9d2..54d6746 100644 ---- a/pyblish_starter/plugins/extract_model.py -+++ b/pyblish_starter/plugins/extract_model.py -@@ -47,5 +47,6 @@ class ExtractStarterModel(api.InstancePlugin): - - # Store reference for integration - instance.data["privateDir"] = dirname -+ instance.data["filename"] = filename - - self.log.info("Extracted {instance} to {path}".format(**locals())) -diff --git a/pyblish_starter/plugins/extract_rig.py b/pyblish_starter/plugins/extract_rig.py -index f107040..4766a26 100644 ---- a/pyblish_starter/plugins/extract_rig.py -+++ b/pyblish_starter/plugins/extract_rig.py -@@ -51,5 +51,6 @@ class ExtractStarterRig(api.InstancePlugin): - - # Store reference for integration - instance.data["privateDir"] = dirname -+ instance.data["filename"] = filename - - self.log.info("Extracted {instance} to {path}".format(**locals())) -diff --git a/pyblish_starter/plugins/integrate_asset.py b/pyblish_starter/plugins/integrate_asset.py -index 6ae21f9..032554e 100644 ---- a/pyblish_starter/plugins/integrate_asset.py -+++ b/pyblish_starter/plugins/integrate_asset.py -@@ -14,7 +14,9 @@ class IntegrateStarterAsset(api.InstancePlugin): - - def process(self, instance): - import os -+ import json - import shutil -+ import pyblish_starter - - privatedir = instance.data.get("privateDir") - assert privatedir, ( -@@ -30,11 +32,38 @@ class IntegrateStarterAsset(api.InstancePlugin): - except OSError: - pass - -- versions = len(os.listdir(instancedir)) -- next_version = "v%03d" % (versions + 1) -- versiondir = os.path.join(instancedir, next_version) -+ version = len(os.listdir(instancedir)) + 1 -+ versiondir = os.path.join( -+ instancedir, -+ pyblish_starter.format_version(version) -+ ) - - shutil.copytree(privatedir, versiondir) - -- self.log.info("Successfully integrated %s to %s" % ( -+ # Update metadata -+ fname = os.path.join(versiondir, ".metadata.json") -+ -+ try: -+ with open(fname) as f: -+ metadata = json.load(f) -+ except IOError: -+ metadata = { -+ "schema": "pyblish-starter:version-1.0", -+ "version": version, -+ "representations": list() -+ } -+ -+ filename = instance.data["filename"] -+ name, ext = os.path.splitext(filename) -+ metadata["representations"].append( -+ { -+ "format": ext, -+ "path": "{version}/%s" % filename -+ } -+ ) -+ -+ with open(fname, "w") as f: -+ json.dump(metadata, f) -+ -+ self.log.info("Successfully integrated \"%s\" to \"%s\"" % ( - instance, versiondir)) -diff --git a/pyblish_starter/tests/test_pipeline.py b/pyblish_starter/tests/test_pipeline.py -index 5c1b470..3c4ff46 100644 ---- a/pyblish_starter/tests/test_pipeline.py -+++ b/pyblish_starter/tests/test_pipeline.py -@@ -1,16 +1,21 @@ - import os - import sys -+import json -+import types - import shutil - import tempfile - - import pyblish_starter - -+from nose.tools import assert_equals -+ - self = sys.modules[__name__] - - - def setup(): - self.tempdir = tempfile.mkdtemp() -- sys.stdout.write("Created temporary directory \"%s\"" % self.tempdir) -+ _register_host() -+ _generate_fixture() - - - def teardown(): -@@ -18,16 +23,125 @@ def teardown(): - sys.stdout.write("Removed temporary directory \"%s\"" % self.tempdir) - - --def test_ls(): -- """ls() returns available assets from current root directory""" -+def _register_host(): -+ host = types.ModuleType("Test") -+ host.__dict__.update({ -+ "root": lambda: self.tempdir, -+ "creator": lambda *args, **kwargs: None, -+ "loader": lambda *args, **kwargs: None, -+ "supported_formats": [".ma"] -+ }) -+ -+ pyblish_starter.register_host(host) -+ -+ -+def _generate_fixture(): - root = os.path.join( - self.tempdir, - "public" - ) -- -- for asset in ("Asset1", "Asset2"): -- os.makedirs(os.path.join(root, asset)) -- -- pyblish_starter.register_root(self.tempdir) -- -- assert pyblish_starter.ls() == ["Asset1", "Asset2"] -+ -+ for asset in ("Asset1",): -+ assetdir = os.path.join(root, asset) -+ os.makedirs(assetdir) -+ -+ if asset == "BadAsset": -+ # An asset must have at least one version -+ continue -+ -+ for version in ("v001",): -+ versiondir = os.path.join(assetdir, version) -+ os.makedirs(versiondir) -+ -+ fname = os.path.join(versiondir, asset + ".ma") -+ open(fname, "w").close() # touch -+ -+ fname = os.path.join(versiondir, ".metadata.json") -+ -+ with open(fname, "w") as f: -+ json.dump({ -+ "schema": "pyblish-starter:version-1.0", -+ "name": asset, -+ "path": versiondir, -+ "representations": [ -+ { -+ "format": ".ma", -+ "source": "{project}/maya/scenes/scene.ma", -+ "author": "marcus", -+ "path": "{dirname}/%s{format}" % asset, -+ }, -+ ] -+ }, f) -+ -+ -+def test_ls(): -+ """ls() returns available assets from current root directory -+ -+ ls() returns a formatted list of available assets. For an asset -+ to be recognised as an asset, it must adhere to a strict schema. -+ -+ ________________________________ ________________________________ -+ | | | | | | | -+ | version1 | version2 | version3 | version1 | version2 | version3 | -+ |__________|__________|__________|__________|__________|__________| -+ | | | -+ | asset1 | asset2 | -+ |________________________________|________________________________| -+ | | -+ | project | -+ |_________________________________________________________________| -+ -+ This schema is located within each version of an asset and is -+ denoted `pyblish-starter:version-1.0`. -+ -+ The members of this schema is also strict, they are: -+ -+ { -+ "schema": "pyblish-starter:version-1.0", -+ "name": "Name of asset", -+ "representations": [List of representations], -+ } -+ -+ Where each `representation` follows the following schema. -+ -+ { -+ "format": "file extension or similar identifier", -+ "source": "absolute path to source file", -+ "author": "original author of version", -+ "path": "relative path to file, starting at {root}" -+ } -+ -+ The returned dictionary is also strictly defined. -+ -+ """ -+ -+ asset = next(pyblish_starter.ls()) -+ reference = { -+ "schema": "pyblish-starter:asset-1.0", -+ "name": "Asset1", -+ "versions": [ -+ { -+ "version": 1, -+ "path": os.path.join(self.tempdir, "public", "Asset1/v001"), -+ "representations": [ -+ { -+ "format": ".ma", -+ "path": "{dirname}/Asset1{format}" -+ } -+ ] -+ }, -+ ] -+ } -+ -+ # Printed on error -+ print("# Comparing result:") -+ print(json.dumps(asset, indent=4, sort_keys=True)) -+ print("# With reference:") -+ print(json.dumps(reference, indent=4, sort_keys=True)) -+ -+ assert_equals(asset, reference) -+ -+ -+def test_ls_returns_sorted_versions(): -+ """Versions returned from ls() are alphanumerically sorted""" -+ assert False -diff --git a/pyblish_starter/tools/asset_loader/app.py b/pyblish_starter/tools/asset_loader/app.py -index 2ee7f46..05a5c34 100644 ---- a/pyblish_starter/tools/asset_loader/app.py -+++ b/pyblish_starter/tools/asset_loader/app.py -@@ -9,7 +9,36 @@ self = sys.modules[__name__] - self._window = None - - -+# Custom roles -+ItemRole = QtCore.Qt.UserRole + 1 -+ -+ - class Window(QtWidgets.QDialog): -+ """Basic asset loader interface -+ -+ _________________________ -+ | | -+ | Assets | -+ | ______________________ | -+ | | | | -+ | | Asset 1 | | -+ | | Asset 2 | | -+ | | ... | | -+ | | | | -+ | | | | -+ | | | | -+ | | | | -+ | | | | -+ | | | | -+ | |______________________| | -+ | ______________________ | -+ | | | | -+ | | Load | | -+ | |______________________| | -+ |__________________________| -+ -+ """ -+ - def __init__(self, parent=None): - super(Window, self).__init__(parent) - self.setWindowTitle("Asset Loader") -@@ -91,15 +120,17 @@ class Window(QtWidgets.QDialog): - def refresh(self): - listing = self.data["model"]["listing"] - -- assets = pipeline.ls() -+ has_assets = False -+ -+ for asset in pipeline.ls(): -+ item = QtWidgets.QListWidgetItem(asset["name"]) -+ item.setData(QtCore.Qt.ItemIsEnabled, True) -+ item.setData(ItemRole, asset) -+ listing.addItem(item) - -- if assets: -- for asset in assets: -- item = QtWidgets.QListWidgetItem(asset) -- item.setData(QtCore.Qt.ItemIsEnabled, True) -- listing.addItem(item) -+ has_assets = True - -- else: -+ if not has_assets: - item = QtWidgets.QListWidgetItem("No assets found") - item.setData(QtCore.Qt.ItemIsEnabled, False) - listing.addItem(item) -@@ -114,15 +145,23 @@ class Window(QtWidgets.QDialog): - item = listing.currentItem() - - if item is not None: -+ asset = item.data(ItemRole) -+ - try: -- pipeline.registered_host().loader(item.text()) -+ pipeline.registered_host().loader(asset) -+ -+ except ValueError as e: -+ error_msg.setText(str(e)) -+ error_msg.show() -+ raise - - except NameError as e: - error_msg.setText(str(e)) - error_msg.show() - raise - -- except (TypeError, RuntimeError, KeyError) as e: -+ # Catch-all -+ except Exception as e: - error_msg.setText("Program error: %s" % str(e)) - error_msg.show() - raise -diff --git a/run_tests.py b/run_tests.py -index 64dfb75..8a3c366 100644 ---- a/run_tests.py -+++ b/run_tests.py -@@ -1,6 +1,8 @@ - import sys --import time - import types -+import warnings -+ -+warnings.filterwarnings("ignore", category=DeprecationWarning) - - maya = types.ModuleType("maya") - maya.mel = types.ModuleType("mel") -diff --git a/setup.py b/setup.py -index 8deb45d..719b808 100644 ---- a/setup.py -+++ b/setup.py -@@ -46,7 +46,7 @@ setup( - classifiers=classifiers, - install_requires=[ - "pyblish-base>=1.4", -- "pyblish-maya>=2.0" -+ "pyblish-maya>=2.1" - ], - entry_points={}, - ) --- -1.7.1 - diff --git a/pyblish_starter/__init__.py b/pyblish_starter/__init__.py index ae1fecf..970bc3a 100644 --- a/pyblish_starter/__init__.py +++ b/pyblish_starter/__init__.py @@ -19,6 +19,8 @@ format_user_dir, format_shared_dir, format_version, + + find_next_version, parse_version, ) @@ -35,5 +37,7 @@ "format_user_dir", "format_shared_dir", "format_version", + + "find_next_version", "parse_version", ] diff --git a/pyblish_starter/lib.py b/pyblish_starter/lib.py index d75e965..9937795 100644 --- a/pyblish_starter/lib.py +++ b/pyblish_starter/lib.py @@ -26,7 +26,7 @@ def listdir(dirname): def time(): """Return file-system safe string of current date and time""" - return datetime.datetime.now().strftime("%Y-%m-%dT%H-%M-%SZ") + return datetime.datetime.now().strftime("%Y%m%dT%H%M%SZ") def format_shared_dir(root): diff --git a/pyblish_starter/maya/lib.py b/pyblish_starter/maya/lib.py index 52aabee..dbedece 100644 --- a/pyblish_starter/maya/lib.py +++ b/pyblish_starter/maya/lib.py @@ -80,3 +80,32 @@ def export_alembic(nodes, file, frame_range=None, uv_write=True): mel_cmd = "AbcExport -j \"{0}\"".format(mel_args_string) return mel.eval(mel_cmd) + + +def imprint(node, data): + """Write `data` to `node` as userDefined attributes + + Arguments: + node (str): Long name of node + data (dict): Dictionary of key/value pairs + + """ + + for key, value in data.items(): + if isinstance(value, bool): + add_type = {"attributeType": "bool"} + set_type = {"keyable": False, "channelBox": True} + elif isinstance(value, basestring): + add_type = {"dataType": "string"} + set_type = {"type": "string"} + elif isinstance(value, int): + add_type = {"attributeType": "long"} + set_type = {"keyable": False, "channelBox": True} + elif isinstance(value, float): + add_type = {"attributeType": "double"} + set_type = {"keyable": False, "channelBox": True} + else: + raise TypeError("Unsupported type: %r" % type(value)) + + cmds.addAttr(node, longName=key, **add_type) + cmds.setAttr(node + "." + key, value, **set_type) diff --git a/pyblish_starter/maya/pipeline.py b/pyblish_starter/maya/pipeline.py index 1fb76cf..cb8f59a 100644 --- a/pyblish_starter/maya/pipeline.py +++ b/pyblish_starter/maya/pipeline.py @@ -7,6 +7,8 @@ from maya import cmds, mel +from . import lib + self = sys.modules[__name__] self.log = logging.getLogger() self.menu = "pyblishStarter" @@ -76,15 +78,17 @@ def root(): def load(asset, version=-1): """Load asset + + Arguments: - asset (dict): Object of schema "pyblish-starter:asset-1.0" + asset ("pyblish-starter:asset-1.0"): Asset which to import version (int, optional): Version number, defaults to latest Returns: Reference node Raises: - IndexError on version not found + IndexError on no version found ValueError on no supported representation """ @@ -104,15 +108,17 @@ def load(asset, version=-1): # Such as choosing between `.obj` and `.ma` and `.abc`, # each compatible but different. try: - representation = next(rep for rep in version["representations"] - if rep["format"] in supported_formats) + representation = next( + rep for rep in version["representations"] + if rep["format"] in supported_formats + ) + except StopIteration: + formats = list(r["format"] for r in version["representations"]) raise ValueError( - "No supported representations for %s\n" - "Supported representations: %s" % ( - asset["name"], - ", ".join(r["format"] for r in version["representations"])) - ) + "No supported representations for \"%s\"\n\n" + "Supported representations: %s" + % (asset["name"], "\n- ".join(formats))) fname = representation["path"].format( dirname=version["path"], @@ -121,18 +127,30 @@ def load(asset, version=-1): nodes = cmds.file(fname, namespace=asset["name"] + "_", - reference=True) + reference=True, + returnNewNodes=True) + + self.log.info("Containerising \"%s\".." % fname) + containerise(asset["name"], nodes, version) - return cmds.referenceQuery(nodes, referenceNode=True) + self.log.info("Container created, returning reference node.") + return cmds.referenceQuery(nodes[0], referenceNode=True) def create(name, family): """Create new instance + Associate nodes with a name and family. These nodes are later + validated, according to their `family`, and integrated into the + shared environment, relative their `name`. + + Data relative each family, along with default data, are imprinted + into the resulting objectSet. This data is later used by extractors + and finally asset browsers to help identify the origin of the asset. + Arguments: name (str): Name of instance family (str): Name of family - use_selection (bool): Use selection to create this instance? """ @@ -145,42 +163,28 @@ def create(name, family): print("%s + %s" % (pipeline.registered_data(), item.get("data", []))) data = pipeline.registered_data() + item.get("data", []) + + # Convert to dictionary + data = dict((i["key"], i["value"]) for i in data) + instance = "%s_SEL" % name if cmds.objExists(instance): raise NameError("\"%s\" already exists." % instance) - # Include selection instance = cmds.sets(name=instance) - for item in data: - key = item["key"] - + # Resolve template + for key, value in data.items(): try: - value = item["value"].format( + data[key] = value.format( name=name, family=family ) except KeyError as e: raise KeyError("Invalid dynamic property: %s" % e) - if isinstance(value, bool): - add_type = {"attributeType": "bool"} - set_type = {"keyable": False, "channelBox": True} - elif isinstance(value, basestring): - add_type = {"dataType": "string"} - set_type = {"type": "string"} - elif isinstance(value, int): - add_type = {"attributeType": "long"} - set_type = {"keyable": False, "channelBox": True} - elif isinstance(value, float): - add_type = {"attributeType": "double"} - set_type = {"keyable": False, "channelBox": True} - else: - raise TypeError("Unsupported type: %r" % type(value)) - - cmds.addAttr(instance, ln=key, **add_type) - cmds.setAttr(instance + "." + key, value, **set_type) + lib.imprint(instance, data) cmds.select(instance, noExpand=True) @@ -190,6 +194,39 @@ def create(name, family): return instance +def containerise(name, nodes, version): + """Bundle `nodes` into an assembly and imprint it with metadata + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + name (str): Name of resulting assembly + nodes (list): Long names of nodes to containerise + version (pyblish-starter:version-1.0): Current version + + """ + + assemblies = cmds.ls(nodes, assemblies=True) + container = cmds.group(assemblies, name=name) + + for key, value in ( + ("id", "pyblish.starter.container"), + ("author", version["author"]), + ("loader", self.__name__), + ("time", version["time"]), + ("comment", version.get("comment", "")), + ): + + if not value: + continue + + cmds.addAttr(container, longName=key, dataType="string") + cmds.setAttr(container + "." + key, value, type="string") + + return container + + def _display_missing_dependencies(): import pyblish @@ -212,26 +249,27 @@ def _display_missing_dependencies(): messagebox.setWindowTitle("Uh oh") messagebox.setText("Missing dependencies") - messagebox.setInformativeText("""\ -pyblish-starter requires pyblish-maya.\ -""") - - messagebox.setDetailedText("""\ -1) Install Pyblish for Maya - - $ pip install pyblish-maya - -2) Run setup() - - >>> import pyblish_maya - >>> pyblish_maya.setup() - -3) Try again. - - >>> pyblish_starter.install() + messagebox.setInformativeText( + "pyblish-starter requires pyblish-maya.\n" + ) -See https://github.com/pyblish/pyblish-starter for more information. -""") + messagebox.setDetailedText( + "1) Install Pyblish for Maya\n" + "\n" + "$ pip install pyblish-maya\n" + "\n" + "2) Run setup()\n" + "\n" + ">>> import pyblish_maya\n" + ">>> pyblish_maya.setup()\n" + "\n" + "3) Try again.\n" + "\n" + ">>> pyblish_starter.install()\n" + + "See https://github.com/pyblish/pyblish-starter " + "for more information." + ) messagebox.setStandardButtons(messagebox.Ok) messagebox.exec_() diff --git a/pyblish_starter/pipeline.py b/pyblish_starter/pipeline.py index 97bae94..6f96ede 100644 --- a/pyblish_starter/pipeline.py +++ b/pyblish_starter/pipeline.py @@ -95,6 +95,29 @@ def uninstall(): self.log.info("Successfully uninstalled Pyblish Starter!") +def register_default_data(): + register_data(key="id", value="pyblish.starter.instance") + register_data(key="name", value="{name}") + register_data(key="family", value="{family}") + + +def register_default_families(): + register_family( + name="starter.model", + help="Polygonal geometry for animation" + ) + + register_family( + name="starter.rig", + help="Character rig" + ) + + register_family( + name="starter.animation", + help="Pointcache" + ) + + def ls(): """List available assets @@ -181,29 +204,6 @@ def ls(): yield asset_entry -def register_default_data(): - register_data(key="id", value="pyblish.starter.instance") - register_data(key="name", value="{name}") - register_data(key="family", value="{family}") - - -def register_default_families(): - register_family( - name="starter.model", - help="Polygonal geometry for animation" - ) - - register_family( - name="starter.rig", - help="Character rig" - ) - - register_family( - name="starter.animation", - help="Pointcache" - ) - - def register_format(format): self._registered_formats.append(format) diff --git a/pyblish_starter/plugins/collect_instances.py b/pyblish_starter/plugins/collect_instances.py index e63e6c5..c306280 100644 --- a/pyblish_starter/plugins/collect_instances.py +++ b/pyblish_starter/plugins/collect_instances.py @@ -61,7 +61,6 @@ def process(self, context): objectsOnly=True): # Return objectSet, rather # than its members - # print("cmds: %s" % cmds) if not cmds.objExists(objset + ".id"): continue @@ -86,10 +85,10 @@ def process(self, context): # such as mesh and color attributes. These # are considered non-essential to this # particular publishing pipeline. - continue + value = None instance.data[attr] = value # Produce diagnostic message for any graphical # user interface interested in visualising it. - self.log.info("Found: %s " % objset) + self.log.info("Found: \"%s\" " % instance.data["name"]) diff --git a/pyblish_starter/plugins/extract_animation.py b/pyblish_starter/plugins/extract_animation.py index f11deb4..bd3e3b7 100644 --- a/pyblish_starter/plugins/extract_animation.py +++ b/pyblish_starter/plugins/extract_animation.py @@ -1,5 +1,4 @@ from pyblish import api -import pyblish_starter as starter class ExtractStarterAnimation(api.InstancePlugin): @@ -21,13 +20,14 @@ class ExtractStarterAnimation(api.InstancePlugin): def process(self, instance): import os from maya import cmds + from pyblish_starter import format_user_dir from pyblish_starter.maya import export_alembic self.log.debug("Loading plug-in..") cmds.loadPlugin("AbcExport.mll", quiet=True) self.log.info("Extracting animation..") - dirname = starter.format_user_dir( + dirname = format_user_dir( root=instance.context.data["workspaceDir"], name=instance.data["name"]) @@ -36,7 +36,7 @@ def process(self, instance): except OSError: pass - filename = "%s.abc" % instance + filename = "{name}.ma".format(**instance.data) export_alembic( nodes=instance, @@ -47,7 +47,9 @@ def process(self, instance): ) # Store reference for integration - instance.data["userDir"] = dirname - instance.data["filename"] = filename + instance.data.update({ + "userDir": dirname, + "filename": filename, + }) self.log.info("Extracted {instance} to {dirname}".format(**locals())) diff --git a/pyblish_starter/plugins/extract_model.py b/pyblish_starter/plugins/extract_model.py index 05ea344..6f0840a 100644 --- a/pyblish_starter/plugins/extract_model.py +++ b/pyblish_starter/plugins/extract_model.py @@ -1,5 +1,4 @@ from pyblish import api -import pyblish_starter as starter class ExtractStarterModel(api.InstancePlugin): @@ -11,7 +10,7 @@ class ExtractStarterModel(api.InstancePlugin): """ - label = "Extract starter model" + label = "Extract model" order = api.ExtractorOrder hosts = ["maya"] families = ["starter.model"] @@ -19,9 +18,10 @@ class ExtractStarterModel(api.InstancePlugin): def process(self, instance): import os from maya import cmds + from pyblish_starter import format_user_dir from pyblish_maya import maintained_selection - dirname = starter.format_user_dir( + dirname = format_user_dir( root=instance.context.data["workspaceDir"], name=instance.data["name"]) @@ -30,7 +30,7 @@ def process(self, instance): except OSError: pass - filename = "%s.ma" % instance + filename = "{name}.ma".format(**instance.data) path = os.path.join(dirname, filename) @@ -43,10 +43,16 @@ def process(self, instance): typ="mayaAscii", exportSelected=True, preserveReferences=False, + + # Construction history inherited from collection + # This enables a selective export of nodes relevant + # to this particular plug-in. constructionHistory=False) # Store reference for integration - instance.data["userDir"] = dirname - instance.data["filename"] = filename + instance.data.update({ + "userDir": dirname, + "filename": filename, + }) self.log.info("Extracted {instance} to {path}".format(**locals())) diff --git a/pyblish_starter/plugins/extract_rig.py b/pyblish_starter/plugins/extract_rig.py index ae8067e..c6c2e23 100644 --- a/pyblish_starter/plugins/extract_rig.py +++ b/pyblish_starter/plugins/extract_rig.py @@ -1,5 +1,4 @@ from pyblish import api -import pyblish_starter as starter class ExtractStarterRig(api.InstancePlugin): @@ -21,11 +20,11 @@ class ExtractStarterRig(api.InstancePlugin): def process(self, instance): import os - from maya import cmds + from pyblish_starter import format_user_dir from pyblish_maya import maintained_selection - dirname = starter.format_user_dir( + dirname = format_user_dir( root=instance.context.data["workspaceDir"], name=instance.data["name"]) @@ -34,7 +33,7 @@ def process(self, instance): except OSError: pass - filename = "%s.ma" % instance + filename = "{name}.ma".format(**instance.data) path = os.path.join(dirname, filename) @@ -50,7 +49,9 @@ def process(self, instance): constructionHistory=True) # Store reference for integration - instance.data["userDir"] = dirname - instance.data["filename"] = filename + instance.data.update({ + "userDir": dirname, + "filename": filename, + }) self.log.info("Extracted {instance} to {path}".format(**locals())) diff --git a/pyblish_starter/plugins/integrate_asset.py b/pyblish_starter/plugins/integrate_asset.py index 00797b1..1f79f0c 100644 --- a/pyblish_starter/plugins/integrate_asset.py +++ b/pyblish_starter/plugins/integrate_asset.py @@ -2,40 +2,54 @@ class IntegrateStarterAsset(api.InstancePlugin): - """Publicise each instance + """Move user data to shared location - Limitations: - - Limited to publishing within a single Maya project + This plug-in exposes your data to others by encapsulating it + into a new version. """ label = "Integrate asset" order = api.IntegratorOrder + families = [ + "starter.model", + "starter.rig", + "starter.animation" + ] def process(self, instance): import os import json + import errno import shutil - import pyblish_starter + from pyblish_starter import ( + format_version, + find_next_version, + ) + + context = instance.context userdir = instance.data.get("userDir") assert userdir, ( "Incomplete instance \"%s\": " "Missing reference to user directory." - % instance) + % instance + ) - root = instance.context.data["workspaceDir"] - instancedir = os.path.join(root, "shared", str(instance)) + root = context.data["workspaceDir"] + instancedir = os.path.join(root, "shared", instance.data["name"]) try: os.makedirs(instancedir) - except OSError: - pass + except OSError as e: + if e.errno != errno.EEXIST: # Already exists + self.log.critical("An unexpected error occurred.") + raise - version = len(os.listdir(instancedir)) + 1 + version = find_next_version(os.listdir(instancedir)) versiondir = os.path.join( instancedir, - pyblish_starter.format_version(version) + format_version(version) ) shutil.copytree(userdir, versiondir) @@ -51,7 +65,14 @@ def process(self, instance): "schema": "pyblish-starter:version-1.0", "version": version, "path": versiondir, - "representations": list() + "representations": list(), + + # Collected by pyblish-base + "time": context.data["date"], + "author": context.data["user"], + + # Collected by pyblish-maya + "source": context.data["currentFile"], } filename = instance.data["filename"] diff --git a/pyblish_starter/plugins/validate_model_hierarchy.py b/pyblish_starter/plugins/validate_model_hierarchy.py new file mode 100644 index 0000000..4c5c1cc --- /dev/null +++ b/pyblish_starter/plugins/validate_model_hierarchy.py @@ -0,0 +1,20 @@ +from pyblish import api + + +class ValidateStarterModelHierarchy(api.InstancePlugin): + """A model hierarchy must reside under a single assembly called "model_GRP" + + - Must reside within `model_GRP` transform + + """ + + label = "Validate model format" + order = api.ValidatorOrder + hosts = ["maya"] + families = ["starter.model"] + + def process(self, instance): + from maya import cmds + + assert cmds.ls(instance, assemblies=True) == ["model_GRP"], ( + "Model must have a single parent called 'model_GRP'.") diff --git a/pyblish_starter/plugins/validate_resources.py b/pyblish_starter/plugins/validate_resources.py index 3ba3942..2e43325 100644 --- a/pyblish_starter/plugins/validate_resources.py +++ b/pyblish_starter/plugins/validate_resources.py @@ -1,7 +1,7 @@ from pyblish import api -class ValidateResources(api.InstancePlugin): +class ValidateStarterResources(api.InstancePlugin): """Resources must not contain absolute paths. When working with external files, such as textures and references, diff --git a/pyblish_starter/plugins/validate_rig_hierarchy.py b/pyblish_starter/plugins/validate_rig_hierarchy.py new file mode 100644 index 0000000..71942a6 --- /dev/null +++ b/pyblish_starter/plugins/validate_rig_hierarchy.py @@ -0,0 +1,20 @@ +from pyblish import api + + +class ValidateStarterRigHierarchy(api.InstancePlugin): + """A rig must reside under a single assembly called "rig_GRP" + + - Must reside within `rig_GRP` transform + + """ + + label = "Validate rig hierarchy" + order = api.ValidatorOrder + hosts = ["maya"] + families = ["starter.rig"] + + def process(self, instance): + from maya import cmds + + assert cmds.ls(instance, assemblies=True) == ["model_GRP"], ( + "Rig must have a single parent called 'model_GRP'.") diff --git a/pyblish_starter/plugins/validate_rig_members.py b/pyblish_starter/plugins/validate_rig_members.py index c6ef7ab..cf0f133 100644 --- a/pyblish_starter/plugins/validate_rig_members.py +++ b/pyblish_starter/plugins/validate_rig_members.py @@ -1,16 +1,17 @@ from pyblish import api -class ValidateStarterRigMembers(api.InstancePlugin): - """A rig must have certain members +class ValidateStarterRigFormat(api.InstancePlugin): + """A rig must have a certain hierarchy and members + - Must reside within `rig_GRP` transform - controls_SEL - cache_SEL - resources_SEL (optional) """ - label = "Validate rig members" + label = "Validate rig format" order = api.ValidatorOrder hosts = ["maya"] families = ["starter.rig"] diff --git a/pyblish_starter/plugins/validate_single_assembly.py b/pyblish_starter/plugins/validate_single_assembly.py new file mode 100644 index 0000000..1666042 --- /dev/null +++ b/pyblish_starter/plugins/validate_single_assembly.py @@ -0,0 +1,26 @@ +from pyblish import api + + +class ValidateStarterSingleAssembly(api.InstancePlugin): + """A rig must have a certain hierarchy and members + + - Must reside within `rig_GRP` transform + - controls_SEL + - cache_SEL + - resources_SEL (optional) + + """ + + label = "Validate single assembly" + order = api.ValidatorOrder + hosts = ["maya"] + families = ["starter.model", "starter.rig"] + + def process(self, instance): + from maya import cmds + assemblies = cmds.ls(instance, assemblies=True) + assert len(assemblies) == 1, ( + ("Multiple assemblies found." + if len(assemblies) > 1 + else "No assembly found") + ) diff --git a/pyblish_starter/tests/test_pipeline.py b/pyblish_starter/tests/test_pipeline.py index eaf6f1f..bb3a3c7 100644 --- a/pyblish_starter/tests/test_pipeline.py +++ b/pyblish_starter/tests/test_pipeline.py @@ -6,7 +6,7 @@ import tempfile import contextlib -import pyblish_starter +from pyblish_starter import pipeline, lib from nose.tools import assert_equals @@ -15,7 +15,7 @@ @contextlib.contextmanager def root(root): - host = pyblish_starter.registered_host() + host = pipeline.registered_host() old = host.root host.root = lambda: root @@ -44,7 +44,7 @@ def _register_host(): "load": lambda *args, **kwargs: None, }) - pyblish_starter.register_host(host) + pipeline.register_host(host) def _generate_fixture(): @@ -57,10 +57,6 @@ def _generate_fixture(): assetdir = os.path.join(root, asset) os.makedirs(assetdir) - if asset == "BadAsset": - # An asset must have at least one version - continue - for version in ("v001",): versiondir = os.path.join(assetdir, version) os.makedirs(versiondir) @@ -73,8 +69,10 @@ def _generate_fixture(): with open(fname, "w") as f: json.dump({ "schema": "pyblish-starter:version-1.0", - "version": pyblish_starter.parse_version(version), + "version": lib.parse_version(version), "path": versiondir, + "time": "", + "author": "mottosso", "representations": [ { "format": ".ma", @@ -134,7 +132,7 @@ def test_ls(): """ - asset = next(pyblish_starter.ls()) + asset = next(pipeline.ls()) reference = { "schema": "pyblish-starter:asset-1.0", "name": "Asset1", @@ -162,7 +160,9 @@ def test_ls(): "scene.ma" ) } - ] + ], + "time": "", + "author": "mottosso", }, ] } @@ -188,4 +188,4 @@ def test_ls_no_shareddir(): os.makedirs(no_shared) with root(no_shared): - assert next(pyblish_starter.ls(), None) is None + assert next(pipeline.ls(), None) is None