What version to use
Currently if you try to search for pyblish you will see 3 versions:
- Pyblish
Base pyblish is fine, but it's a cli based tool and you will have to write your own wrapper around it. And judging from PRs, it's not that well maintained at the moment.
- Pyblish QML
Pyblish QML should be a definitive edition of pyblish with cool GUI, that has lots of features. The problem is that it's even less maintained and works only in very specific environments and with very specific DCC versions. And I've spent too much time before I've realised that it won't work with modern versions of Maya.
- Pyblish Lite
And then we have Pyblish Lite. This is the only plug'n'play solution, right now. It's not perfect, but it has all the pyblish functionality, it has GUI, it has demo project AND it has pretty good documentation. So pyblish-lite is our champion!
Installation
Pyblish Documentation
The install process if very simple. Just run pip command and install pyblish-lite and pyblish-base.
pip install pyblish-lite pyblish-base
After installing it, all you need to start using it is to call method show().
import pyblish_lite
pyblish_lite.show()
This will bring the GUI up and try to find plugins to register.
How to use
Plugins
Pyblish framework is built using plugin architecture. Meaning that each process is independent of each ither. And it has different stages of processing.
Operations for each stage pyblish calls plugins. So whenever you create a new operation (like collecting assets) you're making a new "plugin" for pyblish. And you can have "infinite" amount of said plugins in a chain.
Defining plugins
In order to define a plugin, all you need to do is just inhering from a correct class AND specify the order of operations. Because pyblish has already built-in enums to determine what plugins should follow what. And while you can override it if you want, it will be special case for your production needs. Let's create one of each plugins and see what classes you need.
Collector plugin
It's purposes to gather assets within the scene to process. And it's the first one in the order of operations.
import pyblish.api
from maya import cmds
class CollectPlugin(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder
def process(self, context):
""" This is where you gather assets. It can be as simple as cmds.ls(). But important to create in instance in the context. """
for obj in set(cmds.ls(assemblies=True)):
instance = context.create_instance(obj, family="environment")
It's important to inherit from the ContextPlugin class, since this one creates a context objects that can be shared across multiple stages of processing. And all the asset object instances, are contained within the context. And if you want to exchange information between plugins - context is the way to go.
Validation plugin
This is where we will perform the actual validation functionality over previously gathered meshes.
import pyblish.api
class ValidationPlugin(pyblish.api.InstancePlugin):
order = pyblish.api.ValidatorOrder
def process(self, instance):
""" Whatever logic to validate assets """
You will perform operation over the whole "instance" of the collected object. If your collected object contains multiple meshes (let's say Cube1, Cube2), then with the "instance" argument you would be able to iterate over those within one plugin.
Export and Integration
I'm grouping these two together, because they both inherit from the InstancePlugin. The only thing that really changes within the code is the Order.
import pyblish.api
class ExportPlugin(pyblish.api.InstancePlugin):
order = pyblish.api.ExtractorOrder
def process(self, instance):
""" Whatever logic to export assets """
class IntegratePlugin(pyblish.api.InstancePlugin):
order = pyblish.api.IntegrationOrder
def process(self, instance):
""" Whatever logic to import/integrate """
In my experience I didn't need the Integration part, so I will not touch on it. As for the exporting, i'd just do a basic cmds.file operation most of the time.
Plugin attributes
Plugins have a number of different attributes that determine in which order to execute, what objects to work with and they have some interchangeable data named Context. These are default values whenever you create a new plugin.
hosts = ["*"] # makes plugin work only within specific scenario
families = ["*"] # makes plugin work only with provided list of asset types
targets = ["default"] # works similar to families
label = None # plugin name in the GUI
active = True # is plugin active
version = (0, 0, 0) # version of the plugin
order = -1 # the order of execution
optional = False # can this plugin be turned off
requires = "pyblish>=1" # pyblish version requirement
actions = [] # list of available user actions for the plugin
id = None # Defined by metaclass
match = Intersection # Default matching algorithm. Determines how families and targets are mathced between asset and plugin.
Things that you will update almost every time are usually the order, families, actions. I've covered order already, now what about families? Families is just some sort of filter, that tells pyblish what plugins should be active with what type of collected assets. For example, if you collect joints, you probably want to trigger validators that only work work with rigs. And skip validators that should run on meshes.
The actions array is a list of classes that will be accessible to user, after the validation process is finished. For example, if you ran a validation on mesh and it failed by finding zero length edge. You can create a "fixer" for user. Which they can use, after that validator threw an error. And you can have as many actions as you want for each plugin.
Plugin discovering
Now, how does pyblish even find these plugins? Before running method show(), you need to create a pyblish object, that will contain references to those plugins. And the cool part is that you can just point to the folder where you store all your different validators and it will automatically scan that folder for neccessary classes.
from pathlib import Path
import pyblish.api
import MyMeshCollector, MyMeshExtractor
# Getting rid of demo plugins
pyblish.api.deregister_all_paths()
pyblish.api.deregister_all_plugins()
# Registering Maya as a source
pyblish.api.register_host("maya")
# Registering a path to a folder with all our validators.
plugins_path = Path(__file__).parent
pyblish.api.register_plugin_path(plugins_path.as_posix())
# Registering callback for export
pyblish.api.register_callback("exportValidated", CustomExporterTool)
# Registering individual plugins
pyblish.api.register_plugin(MyMeshCollector)
pyblish.api.register_plugin(MyMeshExtractor)
You can wrap it into a main() function, or save it as a module and just call a module. Or however you desire. I prefer to just call a module whenever I launch Maya.
Make a validator
The most exciting part! Let's make a validator and it should give you good idea on the steps and what you need to do to create your own suite of validators. We will go over the process of making validator that checks for namespaces.
Validator Class Definition
Each validator inherits from pyblish.api.InstancePlugin. My suggestion is that all the validators should have label, active, optional, order and families parameters. __doc__
and actions can be considered optional. Since if we don’t have a description for a plugin or we don’t provide any actions, there’s no point of having them.
And important to define the process() function with the instance argument. This is where our logic is going to be executed during the validation process.
class Namespaces(pyblish.api.InstancePlugin):
__doc__ = """ Checking if there're undesired namespaces."""
label = "Namespace Checker"
active = True
optional =True
order = pyblish.api.ValidatorOrder
families = ["environment"]
actions = []
def process(self, instance):
...
Validation Logic (process method)
The process method contains the core logic for validation. In Pyblish Lite, the success of a validator is implied if no exception is raised during the execution of the process method.
If the function returns None → Validation passed.
If there’s an issue → raise Exception.
class Namespaces(pyblish.api.InstancePlugin):
...
def process(self, instance):
assert ':' not in instance._data["name"], "The name contains a colon. Namespaces were not cleared"
For simple checks we can use asserts, since then it’s clear what’s the expected result. Also you can use python exceptions or some of Pyblish Lite specific exceptions. Not sure what Pyblish specific exceptions add, but there’s a possibility.
self.log.error("Error message into pyblish log")
raise pyblish.api.ValidationError(“Error message”)
#../pyblish/error.py
class PyblishError(Exception):
"""Baseclass for all Pyblish exceptions"""
class ValidationError(PyblishError):
"""Baseclass for validation errors"""
class SelectionError(PyblishError):
"""Baseclass for selection errors"""
class ExtractionError(PyblishError):
"""Baseclass for extraction errors"""
class ConformError(PyblishError):
"""Baseclass for conforming errors"""
class NoInstancesError(Exception):
"""Raised if no instances could be found"""
And during the fail process, we definitely want to use pyblish log, to add message there as well. Because then you can see the validation issue in the console of the GUI.
Actions
Now that you know how to make validators, there's a good question - how do I make actions to allow user fix their issues? Great question! And the answer is quite straightforward! You just inherit from the pyblish.api.Action.
class ActionPlugin(pyblish.api.Action):
def process(self, instance, plugin):
""" Whatever fixing that you want. You'll get access to the instance object of the plugin, that invokes this action """
And in the validator plugin, you just need to specify a reference to this class in the actions list. But don't forget to register the plugin!
class Namespaces(pyblish.api.InstancePlugin):
...
actions = [ActionPlugin]
def process(self, instance):
...
Additional information
These are some printf statements I made to see what data do I get from each operation order. This will help you understand what infromation get's further and what's not.
Collector order
/////// End of Collector ////////
Printing instance._data: {'family': 'rig', 'name': 'Ivan'}
Printing context: [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]
Printing context.data: {'date': 'some_date',
'results': [
{'success': True, 'plugin': <class '...\site-packages\pyblish\plugins\collect_current_date.py.CollectCurrentDate'>, 'instance': None, 'action': None, 'error': None, 'records': [], 'duration': 0.0, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]},
{'success': True, 'plugin': <class '...\site-packages\pyblish\plugins\collect_current_user.py.CollectCurrentUser'>, 'instance': None, 'action': None, 'error': None, 'records': [], 'duration': 0.0, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]},
{'success': True, 'plugin': <class '...\site-packages\pyblish\plugins\collect_current_working_directory.py.CollectCurrentWorkingDirectory'>, 'instance': None, 'action': None, 'error': None, 'records': [], 'duration': 0.0, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]}
],
'user': 'itsme',
'cwd': 'some/path'}
Validator Order
Printing instance.context.data:
{'date': 'some time',
'results': [{'success': True,
'plugin': <class '...\site-packages\pyblish\plugins\collect_current_date.py.CollectCurrentDate'>,
'instance': None,
'action': None,
'error': None,
'records': [],
'duration': 0.0,
'progress': 0,
'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]},
{'success': True,
'plugin': <class '...\site-packages\pyblish\plugins\collect_current_user.py.CollectCurrentUser'>,
'instance': None,
'action': None,
'error': None,
'records': [],
'duration': 0.0,
'progress': 0,
'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]},
{'success': True,
'plugin': <class '...\site-packages\pyblish\plugins\collect_current_working_directory.py.CollectCurrentWorkingDirectory'>,
'instance': None,
'action': None,
'error': None,
'records': [],
'duration': 0.0,
'progress': 0,
'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]},
{'success': True,
'plugin': <class 'pyblish.plugin.CollectRig'>,
'instance': None,
'action': None,
'error': None,
'records': [],
'duration': 16.00050926208496,
'progress': 0,
'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]}],
'user': 'itsme',
'cwd': 'some/path'}
Extractor order
Printing instance._data: {'family': 'rig', 'name': 'Bruce', 'optional': True, 'publish': True, 'label': 'Bruce', '_type': 'instance', '_has_succeeded': True, '_has_failed': False, '_is_idle': False, 'families': ['rig'], '_is_processing': True, '_has_processed': True, 'tempdir': '\\temp\\20041206T143606Z\\rig\\Bruce'}
Printing instance: Bruce
Printing context: [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]
Printing context.data: {'date': 'some time', 'results': [{'success': True, 'plugin': <class '...\site-packages\pyblish\plugins\collect_current_date.py.CollectCurrentDate'>, 'instance': None, 'action': None, 'error': None, 'records': [], 'duration': 0.0, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]}, {'success': True, 'plugin': <class '...\site-packages\pyblish\plugins\collect_current_user.py.CollectCurrentUser'>, 'instance': None, 'action': None, 'error': None, 'records': [], 'duration': 0.0, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]}, {'success': True, 'plugin': <class '...\site-packages\pyblish\plugins\collect_current_working_directory.py.CollectCurrentWorkingDirectory'>, 'instance': None, 'action': None, 'error': None, 'records': [], 'duration': 0.0, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]}, {'success': True, 'plugin': <class 'pyblish.plugin.CollectRig'>, 'instance': None, 'action': None, 'error': None, 'records': [], 'duration': 16.50857925415039, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]}, {'success': True, 'plugin': <class 'pyblish.plugin.ValidateRigContents'>, 'instance': pyblish.plugin.Instance("Bruce"), 'action': None, 'error': None, 'records': [], 'duration': 4.005193710327148, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]}, {'success': True, 'plugin': <class 'pyblish.plugin.ValidateRigContents'>, 'instance': pyblish.plugin.Instance("Ivan"), 'action': None, 'error': None, 'records': [], 'duration': 4.516363143920898, 'progress': 0, 'context': [pyblish.plugin.Instance("Bruce"), pyblish.plugin.Instance("Ivan")]}], 'user': 'itsme', 'cwd': 'some/path'}
Printing instance._data:
{
'family': 'rig',
'name': 'Ivan',
'optional': True,
'publish': True,
'label': 'Ivan',
'_type': 'instance',
'_has_succeeded': True,
'_has_failed': False,
'_is_idle': False,
'families': ['rig'],
'_is_processing': True,
'_has_processed': True,
'tempdir': '\\temp\\20241206T125236Z\\rig\\Ivan'
}
Final words
This article should give you enough information in order to go and start using pyblish in no time. If you want to better understand pyblish architecture, please look into the documentation.
Pyblish architecture documentation
Also I'd suggest to just look into the source code of it. While it's frustrating how badly it's overall documented on the web, the docstrings in the code are actually pretty good. And the whole system is not that complex.
Happ validation and I hope this article was useful to you!