Pyblish 1.2 Released

Summary of new features

  • Integrations
  • Core
    • Feature: Actions (see #142)
    • Feature: Pure-dict data (see #117)
    • Enhancement: Same-name Instances allowed (see #121)
  • 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.

  1. Instead of using instance.data("key"), use instance.data["key"]
  2. Instead of using instance.add("MyObject"), use instance.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() and label.
  • 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

2 Likes

Updated to 1.2.1 in response to bugfix.

  • See here for details.

Great to see the collectors in the UI now:)

Though some feedback from the artists is confusion about the new entries in the UI. Would it be worth looking into a “debug” mode where you have see the collectors?
Artists don’t get anything from Collectors. Or maybe they can in the future?

Hehe, yes, it’s a bit of a double-edged sword, isn’t it?

I’ll have a look at some customisation, they are likely mostly interesting from a debugging point of view. An alternative would be to provide an alternative page with bare essentials, such as how many plug-ins there are, how many failed and such, along with the publish button.

2 Likes

Updated to 1.2.2, with Tray icon and management console.

Tray

A standalone management application for Pyblish to gain an overview of it’s state and current output. It sits quietly in the system tray of the given operating system, collecting output and providing options for debugging and manipulating the currently active instance of the Pyblish front-end, QML.

untitled

Usage

Tray is designed to run during startup of your OS.

Once started, it will sit quietly in the background and listen for interested hosts looking to publish and provide them with a handle to the Pyblish QML front-end.

On Windows, once Pyblish has been installed you can look for the corresponding start menu item called simply “Pyblish”. Clicking on it will launch Pyblish Tray.

You can also launch Tray via the command-line, by running the associated launcher, given you have the required dependencies.

$ pythonw -m pyblish_tray

Which when using Pyblish for Windows looks like this, no dependencies required.

$ cd pyblish-win\bin
$ pyblish-tray
2 Likes

Trying this out installed on the network. Running pyblish-tray.bat works, in that I get the tray item, but the “SHOW” button doesn’t work.

Missing the Distributing new port part, I think.

Thanks for letting me know.

Could you try and run Tray via the command-line and see what gets outputted?

$ cd pyblish-win\bin
$ python -m pyblish_tray

This is after clicking the “SHOW” button as well;

K:\development\tools\pyblish\pyblish-win\Pyblish\bin>python -m pyblish_tray
Launching Pyblish QML..
Finished
Listening for output..
Launching virtual host..
Starting Pyblish..
Traceback (most recent call last):
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\pyblish-x\modules\p
yblish-tray\pyblish_tray\app.py", line 130, in on_object_created
    self.launch_virtual_host()
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\pyblish-x\modules\p
yblish-tray\pyblish_tray\app.py", line 193, in launch_virtual_host
    port = proxy.find_available_port()
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\xmlrpc
lib.py", line 1233, in __call__
    return self.__send(self.__name, args)
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\xmlrpc
lib.py", line 1591, in __request
    verbose=self.__verbose
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\xmlrpc
lib.py", line 1273, in request
    return self.single_request(host, handler, request_body, verbose)
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\xmlrpc
lib.py", line 1301, in single_request
    self.send_content(h, request_body)
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\xmlrpc
lib.py", line 1448, in send_content
    connection.endheaders(request_body)
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\httpli
b.py", line 997, in endheaders
    self._send_output(message_body)
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\httpli
b.py", line 850, in _send_output
    self.send(msg)
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\httpli
b.py", line 812, in send
    self.connect()
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\httpli
b.py", line 793, in connect
    self.timeout, self.source_address)
  File "K:\development\tools\pyblish\pyblish-win\Pyblish\lib\Python27\lib\socket
.py", line 571, in create_connection
    raise err
socket.error: [Errno 10061] No connection could be made because the target machi
ne actively refused it
Spent 533.00 ms creating the application
Listening on 127.0.0.1:9090
Entering state: "hidden"
Entering state: "ready"
Entering state: "clean"
Entering state: "alive"
Polishing: 1893ms
Proxy not initialised

That’s an odd path, what’s is that?

It should be: pyblish-win\bin

Thats what the installer defaults to when pointing to a folder. Where the output with the more expected path;

K:\development\tools\pyblish\pyblish-win\bin>python -m pyblish_tray
Launching Pyblish QML..
Finished
Listening for output..
Launching virtual host..
Starting Pyblish..
Traceback (most recent call last):
  File "K:\development\tools\pyblish\pyblish-win\lib\pyblish-x\modules\pyblish-t
ray\pyblish_tray\app.py", line 130, in on_object_created
    self.launch_virtual_host()
  File "K:\development\tools\pyblish\pyblish-win\lib\pyblish-x\modules\pyblish-t
ray\pyblish_tray\app.py", line 193, in launch_virtual_host
    port = proxy.find_available_port()
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\xmlrpclib.py",
 line 1233, in __call__
    return self.__send(self.__name, args)
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\xmlrpclib.py",
 line 1591, in __request
    verbose=self.__verbose
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\xmlrpclib.py",
 line 1273, in request
    return self.single_request(host, handler, request_body, verbose)
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\xmlrpclib.py",
 line 1301, in single_request
    self.send_content(h, request_body)
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\xmlrpclib.py",
 line 1448, in send_content
    connection.endheaders(request_body)
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\httplib.py", l
ine 997, in endheaders
    self._send_output(message_body)
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\httplib.py", l
ine 850, in _send_output
    self.send(msg)
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\httplib.py", l
ine 812, in send
    self.connect()
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\httplib.py", l
ine 793, in connect
    self.timeout, self.source_address)
  File "K:\development\tools\pyblish\pyblish-win\lib\Python27\lib\socket.py", li
ne 571, in create_connection
    raise err
socket.error: [Errno 10061] No connection could be made because the target machi
ne actively refused it
Spent 388.00 ms creating the application
Listening on 127.0.0.1:9090
Entering state: "hidden"
Entering state: "ready"
Entering state: "clean"
Entering state: "alive"
Polishing: 852ms
Proxy not initialised

Hmmm, had another user logged in which explains the “machine refusal”. Running python -m pyblish_tray works, but pyblish_tray.bat still doesn’t.

Need to run, so the rest of the trouble will have to wait for Monday. Have a good weekend:)

Oh, are you using the .exe installer, pointing to your python-win Git repository? They aren’t meant to be mixed; it’s either or.

If you use the installer, you won’t need the Git repository pyblish-win.

In either case, I might know what it is. It sounds related to the delay you get when running on a network. Will take a look next week. :slight_smile:

It isn’t a git repo, just old habitsâ˜ș

Just tried it again today, and it all works:) Think it was because of that other user logged in.

I expect it to work sporadically, because I suspect it has to do with a timing issue; i.e. the delay involved in launching Tray followed by the immediate request to connect with it.

In any case, I just pushed a fix for 1.2.2 and it should be available as an installer in a few moments or immediately as a Git repo!

Updated to latest with the git repo, and everything seems to work perfectly:)

2 Likes

I have to agree with this. As nice as it is for developer, it is very confusing and overwhelming for artists. I dare to say that at this point with addition of actions, pyblish capabilities are way ahead its GUI representation. I’m deploying full pyblish/ftrack pipeline to a new client next week and their comments (as well as our artists) are that they like the way it works but are worries that the amount of plugins and information visible will be a major overkill to the artists (it gets very cluttered when more plugins and collectors are in play).

So I’d very much like to see the next development to be towards better GUI as was already discussed months ago in the github issues and this topic. When we’re at it, user fields would be up there with GUI on the top of my priorities for future developments.

Thanks @mkolar, great input!

I’m working on a developer guide at the moment that I hope should inspire some action in terms of adding features and fixing problems on your own. Developing an alternative simpler GUI sounds like an excellent starting point too, I’ll include something about it in the guide.

Great !