1.1.0
Pyblish 1.1
Version 1.1 is now available and includes the following new features.
Dependency Injection
The most prominent change is this.
Before illustrating how it works, it's important to point out that this is the new way of writing plug-ins. It means that the current way of implementing plug-ins still works, but are to be considered deprecated and no longer supported.
Now, on to the fun stuff!
As Usual
import pyblish.api
class ValidateInstances(pyblish.api.Validator):
def process(self, instance):
pass
This does as you would expect. Which is to process once for every instance, regardless of family (see below for new family defaults).
import pyblish.api
class SelectInstances(pyblish.api.Selector):
def process(self, context):
pass
In the same spirit, this plug-in runs once and has access to the context. This is nothing new.
Plug-in Independency
What is new is that you can choose to process nothing.
import pyblish.api
class SimpleExtractScene(pyblish.api.Extractor):
def process(self):
cmds.file("myfile.mb", exportAll=True)
This plug-in runs once, as when processing the context
, but doesn't have access to either the current Instance
nor Context
. This can be useful for plug-ins that are completely independent of it's environment and state.
Default Services
What is also new is that you can also request other so-called "services".
import pyblish.api
class CustomValidator(pyblish.api.Validator):
def process(self, user, time):
fname = "myfile_v001_%s_%s.mb" % (user, time())
cmds.file(fname, exportAll=True)
In which case the services user
, time
and config
are injected into the plug-in during process. Each of which are default services that ship with the Pyblish base install.
Here are all of them.
register_service("user", getpass.getuser())
register_service("time", pyblish.lib.time)
register_service("config", pyblish.api.config)
user
is available by value, as it is not expected to change at run-time.time
is callable, as it provides unique values each time it is usedconfig
is shorthand topyblish.api.config
User Defined Services
You can also register your own services..
def say(something, yell=False):
print(something.upper() if yell else something)
pyblish.api.register_service(say)
..and then request them via your plug-ins.
import pyblish.api
class ValidateUniverse(pyblish.api.Validator):
def process(self, say):
say("I just wanted to say, Hello World!", yell=True)
Service Coordination
Services are shared amongst plug-ins.
datastore = {"softFailure": False}
pyblish.api.register_service("store", datastore)
Which means you can use it to communicate and pass information inbetween them.
class ValidatePrimary(pyblish.api.Validator):
def process(self, instance, store):
if instance.has_data("I'm kind of valid.."):
store["softFailure"] = True
class ValidateBackup(pyblish.api.Validator):
def process(self, instance, store):
if store["softFailure"] is True:
# Do alternate validation
Or to provide globally accessible data, such as a database connection.
import ftrack
import pyblish.api
proxy = ftrack.ServerProxy("http://myaddress.ftrack.com")
pyblish.api.register_service("ftrack", proxy)
Distributed Development
With services, you can defer development of a plug-in between more than a single developer allowing for faster iteration times and improved version control.
As a plug-in is developed, requirements may arise..
import pyblish.api
class ValidateSecretSauce(pyblish.api.Validator):
def process(self, instance, sauce):
if sauce.hot():
assert instance.has_data("hotCompatible"), "Sauce too hot!"
assert instance.data("tasty") == True, "Sauce not tasty!"
Which can later be developed.
import pyblish.api
import my_studio_tools
class Sauce(object):
def hot(self):
return my_studio_tools.hot_sauce()
pyblish.api.register_service("sauce", Sauce())
Testing
As a final and important piece to the puzzle, dependency injection makes testing easier and more controllable.
def test_plugin():
"""Testing of a host-dependent plug-in with DI"""
instances = list()
class SelectCharacters(pyblish.api.Validator):
def process(self, context, host):
for char in host.ls("*_char"):
instance = context.create_instance(char, family="character")
instance.add(host.listRelatives(char))
instances.append(instance.name)
class HostMock(object):
def ls(self, query):
return ["bobby_char", "rocket_char"]
def listRelatives(self, node):
if node == "bobby_char":
return ["arm", "leg"]
if node == "rocket_char":
return ["propeller"]
return []
pyblish.api.register_service("host", HostMock())
for result in pyblish.logic.process(
func=pyblish.plugin.process,
plugins=[SelectCharacters],
context=pyblish.api.Context()):
assert_equals(result["error"], None)
assert_equals(len(instances), 2)
assert_equals(instances, ["bobby_char", "rocket_char"])
Related
Simple Plug-ins
A new type of plug-in has been introduced; the superclass of the currently available Selection, Validation, Extraction and Conform (SVEC) plug-ins - called simply Plugin
.
import pyblish.api
class MyPlugin(pyblish.api.Plugin):
def process(self):
self.log.info("I'm simple")
Simple plug-ins are just that, a simpler, less assuming variant of SVEC. Each inherit the exact same functionality and can be used in-place of any other plug-in, the only difference being a default order of -1
meaning they will run before any of it's siblings.
Other than that, they are identical in every way. To make it into a Selector, simply assign it an appropriate order.
class MySelector(pyblish.api.Plugin):
order = pyblish.api.Selector.order
More importantly, simple plug-ins are meant as a stepping stone for beginners not yet familiar with SVEC, and as a bridge to unforseen use of Pyblish, allowing arbitrary use and boundless extensibility.
With this, Pyblish is now a generic automation framework with SVEC becoming a collection of best practices and recommended method of writing plug-ins.
Related
In-memory plug-ins
This new feature will allow you to debug, share and prototype plug-ins more quickly.
In-memory plug-ins come directly from the Python run-time and isn't dependent on a filesystem search, which means you can easily post a plug-in to someone else. Once registered, it will be processed like any other plug-in.
import pyblish.api
class SelectPrototypeInstance(pyblish.api.Selector):
def process(self, context):
instance = context.create_instance("Prototype Test")
instance.set_data("family", "prototype")
pyblish.api.register_plugin(SelectPrototypeInstance)
This can be done anytime prior to publishing, including whilst the GUI is open, which means you can do some pretty intense things to hunt down bugs.
import pyblish.api
data = {}
@pyblish.api.log
class MyPlugin(pyblish.api.Plugin):
def process(self):
data["key"] = "value"
pyblish.api.register_plugin(MyPlugin)
After publishing, data
will contain the key key
with value value
. This is an example of a plug-in reaching out into the current execution environment. Something not possible before.
Related functions
- GitHub Issue
- pyblish.api.register_plugin
- pyblish.api.deregister_plugin
- pyblish.api.deregister_all_plugins
- pyblish.api.registered_plugins
New Defaults for Families and Hosts
The attributes families
and hosts
now default to *
, which means that they process everything in sight, unless you tell it not to.
This is different from how it was before, which was to process nothing, unless you told it to.
This has no effect on your current plug-ins, as you were obligated to always provide a family if you wanted it to process, but does mean it is a non-reversible change. So, fingers crossed you like it!
Custom Test
You can now take control over when publishing is cancelled.
Currently, every plug-in is processed, unless the next plug-in is of an order above 2 (i.e. not a Selector nor Validator) and no prior plug-in within the orders 1-2 (i.e. any Validator) have failed.
This is what that test looks like.
def default_test(**vars):
if vars["nextOrder"] >= 2: # If validation is done
for order in vars["ordersWithError"]:
if order < 2: # Were there any error before validation?
return "failed validation"
return
The test is run each time a new plug-in is about to process, and is given a dictionary vars
which is always up to date with the most recent information.
Currently, the only two members of the dictionary is nextOrder
and ordersWithError
, but as need arises more will most likely be added.
create_instance(name, **kwargs)
You can now create instances and assign it a family in one go.
import pyblish.api
context = pyblish.api.Context()
# Before
instance = context.create_instance(name="MyInstance")
instance.set_data("family", "myFamily")
# After
instance = context.create_instance(name="MyInstance", family="myFamily")
And condense some otherwise repeated code into a more minimal variant.
import pyblish.api
context = pyblish.api.Context()
# Before
instance = context.create_instance("MyInstance")
instance.set_data("family", "myFamily")
instance.set_data("color", "blue")
instance.set_data("age", 35)
instance.set_data("height", 1.90)
# After
instance = context.create_instance(
name="MyInstance",
family="myFamily",
color="blue",
age=35,
height=1.90)
Related
Unified Logic
The order in which to process plug-ins, and whether or not to cancel a publish after a failed validation was previously implemented in each user-facing interface. Such as pyblish.util.publish()
, the command-line interface and graphical user interface.
In 1.1, logic has been centralised and is more likely to remain identical across interfaces.
Related
Asset
As a preview, I've also included an undocumented and unsupported method of using the term Asset
in place of Instance
, as discussed here.
It means you can give it a try and get a sense for whether or not it works for you. If it turns out to be a winner, it may potentially become the de-facto new standard of Pyblish.
An example, taken from the forum thread.
import pyblish.api
class SelectCharacters(pyblish.api.Selector):
def process(self, context):
for objset in cmds.ls(type="objectSet"):
name = cmds.getAttr(objset + ".name")
asset = context.create_asset(name)
asset.set_data("family", "character")
class ValidateColor(pyblish.api.Validator):
families = ["character"]
def process(self, asset):
assert asset.data("color") == "blue", "%s isn't blue" % asset
The same behaviour is replicated all across the board, including an alias for Instance
through the API.
>>> import pyblish.api
>>> assert pyblish.api.Instance == pyblish.api.Asset
Use with caution, expect it to vanish at any moment without warning.
That's it! Enjoy!