Summary of new features
- Integrations
- Pyblish for Hiero added
- Core
- GUI
- Enhancement: Collectors now visible
- Actions accessible via right-click on plug-ins.
Overview
The new version focuses on Actions, a flexible method of adding any functionality to plug-ins whilst reaping the benefits of a context-sensitive environment. Associated actions show up alongside plug-ins that already limit themselves to various constraints, such as which project, shot, artist and asset is currently active.
The actions can be further tailored to only appear at the success, failure or done state of a plug-in, providing you with much flexibility in terms of which actions to provide when, such as only enabling the selection of affected nodes in the viewport on a plug-in that had just found a problem with an asset.
Furthermore, you will also notice that collectors are now visible in the GUI as unmodifiable, unchecked items, making it more easy to spot what was actually run and to inspect the various properties of each collector, such as itâs description.
Installation
Whether you are installing anew or updating, it is recommended that you install from scratch.
- For Windows, download and run the installer.
- For Linux, OSX or to install via the command-line, see manual installation instructions here.
If you run into any issues, feel free to post below.
Transition Guide
The only change requiring any additional thought is pure-dict data and the deprecation of add()
on Context and Instance objects. Both of which are fully backwards compatible.
The deprecation of add
means that both Context and Instance are more closely resembling the pure Python list
.
- Instead of using
instance.data("key")
, useinstance.data["key"]
- Instead of using
instance.add("MyObject")
, useinstance.append("MyObject")
Actions
This functionality is meant to replace ârepairâ, along with adding an abundance of flexibility in terms of context-sensitive functionality. Attach any functionality to a plug-in and tailor it to a particular state; like an action only available via a failed validator, or a successful extraction, or just all-round functionality associated with a particular plug-in.
Each action have access to both the Context and itâs parent plug-in via dependency injection, along with any other custom dependencies already available to plug-ins in general.
Actions in QML are arranged in a menu with optional customisable groups and separators. Actions with any kind of implementation error show up as well, including a helpful error message for simplified debugging.
Dependency Injected
These objects are available via dependency injection by default.
-
context
: The global context -
plugin
: The parent plug-in
Full list of features
- Per-plugin actions
- Action API ~= Plug-in API, it is more or less a 1-1 match between their interfaces, including
process()
andlabel
. - Standard logging and exception reporting, identical to plug-ins
- Standard dependency injection still applies; can still inject custom functionality
- Customisable icon per action, from Awesome Icon
- Customisable availability
-
all
: Always -
processed
: After plug-in has been processed -
failed
: After plug-in has been processed, and failed -
succeeded
: After plug-in has been processed, and succeeded
Basic use
class OpenInExplorer(pyblish.api.Action):
label = "Open in Explorer"
on = "failed" # This action is only available on a failed plug-in
icon = "hand-o-up" # Icon from Awesome Icon
def process(self, context):
import subprocess
subprocess.call("start .", shell=True) # Launch explorer at the cwd
class Validate(pyblish.api.Validator):
actions = [
# Order of items is preserved
pyblish.api.Category("My Actions"),
MyAction,
pyblish.api.Separator,
]
def process(self, context, plugin):
"""The Context and parent Plug-in are available via dependency injection"""
self.log.info("Standard log messages apply here.")
raise Exception("Exceptions too.")
Showcase
Every possible combination of an action.
class ContextAction(pyblish.api.Action):
label = "Context action"
def process(self, context):
self.log.info("I have access to the context")
self.log.info("Context.instances: %s" % str(list(context)))
class FailingAction(pyblish.api.Action):
label = "Failing action"
def process(self):
self.log.info("About to fail..")
raise Exception("I failed")
class LongRunningAction(pyblish.api.Action):
label = "Long-running action"
def process(self):
self.log.info("Sleeping for 2 seconds..")
time.sleep(2)
self.log.info("Ah, that's better")
class IconAction(pyblish.api.Action):
label = "Icon action"
icon = "crop"
def process(self):
self.log.info("I have an icon")
class PluginAction(pyblish.api.Action):
label = "Plugin action"
def process(self, plugin):
self.log.info("I have access to my parent plug-in")
self.log.info("Which is %s" % plugin.id)
class LaunchExplorerAction(pyblish.api.Action):
label = "Open in Explorer"
icon = "folder-open"
def process(self, context):
import os
import subprocess
cwd = context.data["cwd"]
self.log.info("Opening %s in Explorer" % cwd)
result = subprocess.call("start .", cwd=cwd, shell=True)
self.log.debug(result)
class ProcessedAction(pyblish.api.Action):
label = "Success action"
icon = "check"
on = "processed"
def process(self):
self.log.info("I am only available on a successful plug-in")
class FailedAction(pyblish.api.Action):
label = "Failure action"
icon = "close"
on = "failed"
class SucceededAction(pyblish.api.Action):
label = "Success action"
icon = "check"
on = "succeeded"
def process(self):
self.log.info("I am only available on a successful plug-in")
class BadEventAction(pyblish.api.Action):
label = "Bad event action"
on = "not exist"
class InactiveAction(pyblish.api.Action):
active = False
class PluginWithActions(pyblish.api.Validator):
optional = True
actions = [
pyblish.api.Category("General"),
ContextAction,
FailingAction,
LongRunningAction,
IconAction,
PluginAction,
pyblish.api.Category("OS"),
LaunchExplorerAction,
pyblish.api.Separator,
FailedAction,
SucceededAction,
pyblish.api.Category("Debug"),
BadEventAction,
InactiveAction,
]
def process(self):
self.log.info("Ran PluginWithActions")
Maya example
import time
import pyblish.api
import pyblish_qml
class Collect(pyblish.api.Collector):
def process(self, context):
i = context.create_instance("MyInstance")
i.data["family"] = "default"
i.append("pCube1")
class SelectInvalidNodes(pyblish.api.Action):
label = "Select broken nodes"
on = "failed"
icon = "hand-o-up"
def process(self, context):
self.log.info("Finding bad nodes..")
nodes = []
for result in context.data["results"]:
if result["error"]:
instance = result["instance"]
nodes.extend(instance)
self.log.info("Selecting bad nodes: %s" % ", ".join(nodes))
cmds.select(deselect=True)
cmds.select(nodes)
class Validate(pyblish.api.Validator):
actions = [
pyblish.api.Category("Scene"),
SelectInvalidNodes
]
def process(self, instance):
raise Exception("I failed")
pyblish.api.register_plugin(Collect)
pyblish.api.register_plugin(Validate)
import pyblish_maya
pyblish_maya.show()
Pure-dict
The data
property of Context and Instance objects is now a standard Python dictionary, with backwards compatibility for being called directly.
import pyblish.api as pyblish
context = pyblish.Context()
# Old behaviour preserved
context.set_data("key", "value")
# New behaviour preferred
context.data["key"] = "value"
if "key" in context.data:
context.data.pop("key")
assert context.data.get("key") == None
# The same applies to Instances
instance = context.create_instance("MyInstance")
instance.data["key"] = "value"
The recommended way forwards is to start transitioning to using pure-dict data, not worrying too much about going back to refactor the old ways. Down the line, there will be a warning printed for each call to a deprecated member so as to easily find and refactor things.
From now on, guides and discussion will start to assume this new way of working.
Same name instances
Initially, you could store multiple instances of the same name in the Context. Later on, a feature was implemented to allow referencing an Instance within a Context by name, meaning this ability had to be removed.
Turns out, allowing same-name instances is useful for when you have the same logical asset in your scene, being treated differently by one or more plug-ins; examples include collecting a character rig for extraction to an animator, but then also collecting it for playblasting of a turntable. The turntable instance would only interest itself with plain meshes, and optional overrides for playblast settings being passed onto the relevant playblasting plug-ins, whereas the former requires every related node in order to perform accurate validation on it.
The result is a restored ability to maintain multiple instances of the same name within the Context, whilst at the same time being able to refer to them by name. The ability to refer to by name is to be considered convenience functionality.
Enjoy!
Best,
Marcus