Understanding Custom Context Usage

I’m new to Pyblish, just starting to look under the hood this week. I have Pyblish up and running in Maya, which seems like it will be an awesome step towards my studio’s asset validation needs. But I’m also trying to see how I can apply Pyblish to work stand alone on the file system. Meaning I’d like to point Pyblish to a folder and validate it’s structure against a number of rules.

When it comes to the Context in Maya, that makes sense, it’s the opened scene ATM. But when it comes to validating a folder, I’d like to make my own context object, add a new instance (which represents the file path) perform my collection and validation (updating the instance) and then pass that context to the pyblish_qml UI.

The script below is what I’ve tried, and when I call show() to open the QML UI, it’s almost as if the context has been forgotten or updated without my custom Instance object (the folder path to validate).

Is there a proper way I should be doing this? Essentially I want to use the UI, but create instances for each folder path to check and make sure those instances are always in the UI even after opening\updating.

Thanks!

import os
import argparse
import pyblish.api
import pyblish.util
import pyblish_qml


def run_path_validation(path):
    if not os.path.exists(path):
        raise Exception("Could not find the supplied path: {} on the hard drive. Ensure it actually exists and is formatted properly.".format(args.path))

    pyblish.api.register_plugin_path(r"D:\plugins")

    context = pyblish.api.Context()
    instance = context.create_instance(name="FilePath")
    instance.data["path"] = path

    pyblish.util.publish(context)

    # Register and show the Pyblish GUI
    pyblish.api.register_gui("pyblish_qml")
    server = pyblish_qml.show()
    server.wait()

parser = argparse.ArgumentParser()
parser.add_argument('-p', "--path", required=True)
if __name__ == "__main__":
    args = parser.parse_args()
    run_path_validation(args.path)

Hi @JasonC, welcome aboard. :slight_smile:

What you’re looking for is a Collector plug-in to generate your instances, like you would also do in Maya. The collector is responsible for populating the context.

Your example has a few misunderstandings.

    context = pyblish.api.Context()
    instance = context.create_instance(name="FilePath")
    instance.data["path"] = path

This is the part you’d want to have happen in a context plug-in. The Context is generally not created by hand, but is passed to you via each plug-in.

    pyblish.util.publish(context)

This actually runs the entire process and finishes, and is meant as a substitute for a UI like Pyblish QML. Pyblish QML will call its own publish() which is then also drawn visually by the UI.

    # Register and show the Pyblish GUI
    pyblish.api.register_gui("pyblish_qml")

The registration only affects what happens when you call…

from pyblish import api
api.show()  # Which UI should I call `.show()` on? In this case, pyblish_qml.show()

But this next call explicitly calls QML, which is ok too. The registration is meant to generalise the UI, for when you have multiple UIs but want for example a single menu item that calls a generic api.show(). Having multiple UIs is exceedingly rare, so you unlikely need to register a UI.

    server = pyblish_qml.show()
    server.wait()

This next call has a command-line equivalent you can use.

# Assuming you have python, PyQt5 or PySide2 and Pyblish + Pyblish QML on your PYTHONPATH
$ python -m pyblish_qml

Which is probably what you are looking for if using QML outside of a DCC. In that case, you’d register plug-ins via the command-line too - including your Collector that collects your folders and files - and then launch QML from that same environment.

$env:PYBLISHPLUGINPATH = "D:\\plugins"
python -m pyblish_qml

And that’ll bring up the UI and call on any collectors at that location.

For a primer on collectors and registering plug-ins in general, I recommend these resource here.

Hey Marcus, Thanks for the thorough reply! I have some follow up questions for you…

Executing the following, it doesn’t look like there is a .show() on pyblish.api. I’m on version 1.8.8
image

Also, when launching QML from the commandline, is there also a way to pass a list of targets along? The same way I’s pass the plugin path using: $env:PYBLISHPLUGINPATH = "D:\\plugins"

You’re right, looking closer it’s coming from the DCC integrations like pyblish-maya.

The idea being that you register one or more GUIs globally, such that it can apply to each integration without the integration needing any special treatment. But yes, as you can see I couldn’t even remember where and how this was used; I’ve never seen this used in practice and can likely safely be considered overengineering. :sweat_smile:

What kind of targets do you have in mind? Normally, you’d include a relevant collector for the task at hand. To pick up image files/folders, you’d pass a image collector. To pick up Alembic files, an Alembic collector etc.

The command-line argument for the UI is rather limited.

The base library has more, for publishing without a UI.

 $ python -m pyblish --help
Usage: pyblish [OPTIONS] COMMAND [ARGS]...

  Pyblish command-line interface

  Use the appropriate sub-command to initiate a publish.

  Use the --help flag of each subcommand to learn more about what it can do.

  Usage:
      $ pyblish publish --help
      $ pyblish test --help

Options:
  --verbose                       Display detailed information. Useful for
                                  debugging purposes.
  --version                       Print the current version of Pyblish
  --paths                         List all available paths
  --plugins                       List all available plugins
  --registered-paths              Print only registered-paths
  --environment-paths             Print only paths added via environment
  -pp, --plugin-path TEXT         Replace all normally discovered paths with
                                  this This may be called multiple times.
  -ap, --add-plugin-path TEXT     Append to normally discovered paths.
  -d, --data TEXT...              Initialise context with data. This takes two
                                  arguments, key and value.
  -ll, --logging-level [debug|info|warning|error|critical]
                                  Specify with which level to produce logging
                                  messages. A value lower than the default
                                  "warning" will produce more messages. This
                                  can be useful for debugging.
  --help                          Show this message and exit.

Commands:
  gui
  publish  Publish instances of path.

Oh and here’s another place the registered UI is used.

python -m pyblish gui

Just happened to work on something similar today.
how i’d tackle it is ignore the context, and instead use the instance feature on collectors.

this runs outside maya, and collects filepaths.
i then run a validator plugin on this to check if the paths conform to conventions.

either create 1 instance containing all paths. (set combine_paths to True)
image

or an instance for each path
image

then simply run validators on the instances of type “path”

here is my code:

import pyblish.api
import pathlib2 as pathlib

class CollectFilePaths(pyblish.api.ContextPlugin):
    label = "Collect FileNames as Path"
    order = pyblish.api.CollectorOrder
    # hosts = ["maya"]
    families = ['paths']

    mesh_folder_path = r'C:\Projects\pyblish-plugin-manager\sample\test files\projects\final fantasia FANTASY RPG\meshes'
    combine_paths = True

    def process(self, context):
        paths = pathlib.Path(self.mesh_folder_path)
        if self.combine_paths:
            instance_controls = context.create_instance("FilePaths", family='paths')
            instance_controls[:] = paths.iterdir()
        else:
            for path in paths.iterdir():
                instance_controls = context.create_instance(str(path), family='paths')
                instance_controls[:] = [path]
import pyblish.api

# register GUI
pyblish.api.register_gui("pyblish_qml")
pyblish.api.register_gui("pyblish_lite")

# from pyblish_qml import api, show
# # Tell QML about dependencies
# api.register_python_executable("C:/Python27/python.exe")
# api.register_pyqt5("C:/modules/python-qt5")

# unregister the default plugins
pyblish.api.deregister_all_plugins()
pyblish.api.deregister_all_paths()

# register my plugin
from sample.validate_folder.CollectFilePaths import CollectFilePaths
pyblish.api.register_plugin(CollectFilePaths)

# EDIT: also register validator here
from sample.validate_folder.ValidateFilePaths import ValidateFilePaths
pyblish.api.register_plugin(ValidateFilePaths)

import pyblish_lite
pyblish_lite.show()

adding a validator plugin that checks if path matched with pattern FF_*.ma

not combining, and shortening the folderpath to just the name, is cleanest visual overview IMO
image

import pyblish.api
import pathlib2 as pathlib

class ValidateFilePaths(pyblish.api.InstancePlugin):
    label = "check FileNames starts with"
    order = pyblish.api.ValidatorOrder
    families = ['paths']

    def process(self, instance):
        assert pathlib.Path(instance.name).match('FF_*.ma')

Thanks again, Marcus. I think I’m starting to understand this a little bit better (maybe). Please tell me if the following would be a correct approach.

Use Case:
Through some custom ui that I build, the user will choose which validation they want to run and on which context (example: Asset Folder validation on folder D:\MyAssets\Asset_01). The user clicks a “Validate” button and the Pyblish UI pops open with the appropriate instance filled out in the context and only the validation plugins loaded needed for this type of test.

Under The Hood:
My custom tool\UI will figure out which plugins are needed for each validation type and run the equivalent of a batch script that does the following:

$env:PYBLISHPLUGINPATH = "D:\\pluginPathToMyDesiredValitationType"
python -m pyblish_qml

The plugin path would contain only the appropriate collector and validator plugins necessary for that type of validation test.

Does this seem like an acceptable approach? or am I overengineering this need by not properly understanding how Pyblish works?