Action on InstancePlugin: "This is a Bug"

Hello
i have a validator that i will be appriciated if you take a look at it and let me know my mistakes in it.
besides it will give me an error in maya that i will write it

i’m using version 1.3.1

import pyblish.api
from maya import cmds, mel


def _get_has_history(instance):
    invalid = []
    for node in cmds.ls(instance, type='mesh'):
        if len(cmds.listHistory(node)) > 1:
            invalid.append(node)
            continue
    return invalid


class ClearHistoryAction(pyblish.api.Action):
    label = "Clear History"
    on = "failed"
    icon = "wrench"

    def process(self, instance):
        """Repair by deleting the history of meshes"""
        invalid = _get_has_history(instance)
        for each in invalid:
            cmds.select(each, r=True)
            mel.eval("DeleteHistory;")
            self.log.info("Success on deleting history of %s" % each)


class ValidateHistory(pyblish.api.InstancePlugin):
    """Your model have History or not..!?"""

    order = pyblish.api.ValidatorOrder
    families = ['anr.model']
    hosts = ['maya']
    optional = True
    label = "Detect History"
    version = (0, 0, 2)

    actions = [
        pyblish.api.Category("Clear History"),
        ClearHistoryAction
    ]

    def process(self, instance):
        invalid = _get_has_history(instance)

        if invalid:
            raise RuntimeError(
                "Meshes found in instance[{0}] with history: {1}".format(instance, invalid))

    # def repair(self, instance):
    #     """Repair by deleting the empty layers"""
    #     invalid = _get_has_history()
    #     for each in invalid:
    #         cmds.select(each, r=True)
    #         mel.eval("DeleteHistory;")
    #         self.log.info("Success on deleting history of %s" % each)

Error:

# Traceback (most recent call last):
#   File "D:\__anr_pipeline\Pipeline\bin\pyblish-win\lib\pyblish\modules\pyblish-rpc\pyblish_rpc\service.py", line 119, in _dispatch
#     return func(*params)
#   File "D:\__anr_pipeline\Pipeline\bin\pyblish-win\lib\pyblish\modules\pyblish-rpc\pyblish_rpc\service.py", line 97, in process
#     action=action)
#   File "D:\__anr_pipeline\Pipeline\bin\pyblish-win\lib\pyblish\modules\pyblish-base\pyblish\plugin.py", line 414, in process
#     return __explicit_process(plugin, context, instance, action)
#   File "D:\__anr_pipeline\Pipeline\bin\pyblish-win\lib\pyblish\modules\pyblish-base\pyblish\plugin.py", line 430, in __explicit_process
#     "Cannot process an InstancePlugin without an instance. This is a bug")
# AssertionError: Cannot process an InstancePlugin without an instance. This is a bug

thanks

Hi @Mahmoodreza_Aarabi,

If you could produce an full example of:

  1. Plug-ins, including collector
  2. Registering plug-ins via pyblish.api.register_plugin(..)
  3. Publishing via pyblish.util.publish()

In a single block of code that illustrates this problem, without using Maya, then I could have a look much quicker. Otherwise, I’ll try and have a look at this later tonight/tomorrow.

It looks like you’ve got the wrong arguments for your Action, it only supports context and plugin.

hey man
i changed it to context, but it does not work.

for getting instance from context i can do this?
instance = context.instance

i used this : context= instance.context and it works

i changed it to this:

class ClearHistoryAction(pyblish.api.Action):
    label = "Clear History"
    on = "failed"
    icon = "wrench"

    def process(self, context):
        """Repair by deleting the history of meshes"""
        instance = context.instance
        invalid = _get_has_history(instance)
        for each in invalid:
            cmds.select(each, r=True)
            mel.eval("DeleteHistory;")
            self.log.info("Success on deleting history of %s" % each)

but it give same error as before.
i don’t know how we can connect Validators and Actions in best way

context is a list of many instances, you can iterate through the instances like this.

for instance in context:
  print(instance)

Have a look at the Maya example here, the action will become visible after validation. See the on property to customise this.

i changed something and it works, but i have some questions:

import pyblish.api
from maya import cmds, mel


def _get_has_history(instance):
    invalid = []
    for node in cmds.ls(instance, type='mesh'):
        if len(cmds.listHistory(node)) > 1:
            invalid.append(node)
            continue
    return invalid


class ClearHistoryAction(pyblish.api.Action):
    label = "Clear History"
    on = "failed"
    icon = "wrench"

    def process(self, context):
        """Repair by deleting the history of meshes"""

        for instance in context:
            invalid = _get_has_history(instance)
            for each in invalid:
                cmds.select(each, r=True)
                mel.eval("DeleteHistory;")
                self.log.info("Success on deleting history of %s" % each)


class ValidateHistory(pyblish.api.ContextPlugin):
    """Your model have History or not..!?"""

    order = pyblish.api.ValidatorOrder
    families = ['anr.model']
    hosts = ['maya']
    optional = True
    label = "Detect History"
    version = (0, 0, 1)

    actions = [
        pyblish.api.Category("Repair"),
        ClearHistoryAction
    ]

    def process(self, context):
        for instance in context:
            invalid = _get_has_history(instance)

            if invalid:
                raise RuntimeError(
                    "Meshes found in instance[{0}] with history: {1}".format(instance, invalid))

i changed class ValidateHistory(pyblish.api.InstancePlugin): to class ValidateHistory(pyblish.api.ContextPlugin):
i don’t know why i should use context when i can use instance instead
can you tell me?

InstancePlugin runs once for every Instance in the Context, whereas ContextPlugin only runs once per publish. If what you want is to validate your instances, use InstancePlugin, otherwise, you can use ContextPlugin.

If you are using ContextPlugin, you can remove the families attribute, it’s only effective when publishing Instances with families.

Just to be clear. In the case of this Validator you don’t want to swap it over to ContextPlugin but keep InstancePlugin. This is because you individually want to validate the contents

This is currently an open issue. It’s a feature that I personally think makes sense, though as described in that issue I’ll have to have a look at what the convenient logic is internally to pyblish.


Actions

The idea with repair actions is actually (if I understood correctly from @marcus) is that you’d take the results from the publish (like validation) and take that information from the context object instead of taking the nodes in the instance again. I do have to admit that I have no idea where to find good examples and real production examples for Actions are sparse.

The feature was originally described with the 1.2 release here showing this 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()

Even though the example is showing a lot of its features and intended workflow what I think is odd is that it’s taking all nodes from all instances where the Action originally was triggered on a specific Plug-in to which it was added. This particular example takes “all instances” even those that might have not run in this plug-in.

What you’d want to do is take the results from the instances in the context that are related to the plug-in it was called on. As such the action would need to take the plugin argument to identify that plug-in.

To do this you’d need to apply some of Pyblish’ own logic from pyblish.logic. The method we need is instances_by_plugin and is also exposed through the pyblish.api so let’s use that.

import time
import pyblish.api
import pyblish_qml


class Collect(pyblish.api.ContextPlugin):
    order = pyblish.api.CollectorOrder
    
    def process(self, context):
        
        i = context.create_instance("MyInstance")
        i.data["family"] = "default"
        i.append("pCube1")
        
        i = context.create_instance("OtherInstance")
        i.data["family"] = "other"
        i.append("pSphere1")


class SelectInvalidNodes(pyblish.api.Action):
    label = "Select broken nodes"
    on = "failed"
    icon = "hand-o-up"
    
    def process(self, plugin, context):
        self.log.info("Finding bad nodes..")
        
        return
        
        # Get the errored instances
        errored_instances = []
        for result in context.data["results"]:
            if result["error"]:
                instance = result["instance"]
                errored_instances.append(instance)
                
        # Apply pyblish.logic to get the instances for the plug-in
        instances = pyblish.api.instances_by_plugin(errored_instances, plugin)
        
        # Get the nodes from the all instances that ran through this plug-in
        nodes = []
        for instance in instances:
            nodes.extend(instance)
        
        self.log.info("Selecting bad nodes: %s" % ", ".join(nodes))
        cmds.select(deselect=True)
        cmds.select(nodes)


class Validate(pyblish.api.InstancePlugin):
    order = pyblish.api.ValidatorOrder
    
    actions = [
        pyblish.api.Category("Scene"),
        SelectInvalidNodes
    ]
    
    families = ["default"]

    def process(self, instance):
        raise Exception("I failed")


pyblish.api.register_plugin(Collect)
pyblish.api.register_plugin(Validate)

import pyblish_maya
pyblish_maya.show()

This is the implementation I would expect to work. But it doesn’t work!
Funnily enough I’m getting the same error:

# Traceback (most recent call last):
#   File "P:\pipeline\dev\git\pyblish-win\lib\pyblish\modules\pyblish-rpc\pyblish_rpc\service.py", line 119, in _dispatch
#     return func(*params)
#   File "P:\pipeline\dev\git\pyblish-win\lib\pyblish\modules\pyblish-rpc\pyblish_rpc\service.py", line 97, in process
#     action=action)
#   File "P:\pipeline\dev\git\pyblish-win\lib\pyblish\modules\pyblish-base\pyblish\plugin.py", line 414, in process
#     return __explicit_process(plugin, context, instance, action)
#   File "P:\pipeline\dev\git\pyblish-win\lib\pyblish\modules\pyblish-base\pyblish\plugin.py", line 430, in __explicit_process
#     "Cannot process an InstancePlugin without an instance. This is a bug")
# AssertionError: Cannot process an InstancePlugin without an instance. This is a bug

@marcus should this actually work?

This was indeed a bug. It has caused Actions not to be usable with Explicit plug-ins; I’m surprised it hasn’t come up earlier, but it’s good we caught this now!

Here’s a small reproduction with no cruft.

import pyblish.api
import pyblish.plugin

class MyAction(pyblish.api.Action):
    def process(self, plugin, context):
        assert False, "Action ran"

class MyValidator(pyblish.api.InstancePlugin):
    order = pyblish.api.ValidatorOrder
    
    actions = [
        pyblish.api.Category("Scene"),
        MyAction,
    ]

context = pyblish.api.Context()
result = pyblish.plugin.process(MyValidator, context, instance=None, action="MyAction")
assert str(result["error"]) == "Action ran"

As a sidenote, explicit plug-ins are not using the dependency injection from Pyblish 1.2, so make sure you have the right argument signature to your actions.

For the impatient, this has been fixed in all but the Windows release. You can pull either pyblish or pyblish-base to get a hold of it!

Thanks @marcus, but i need it on windows

Have a look at installing it plain. pyblish-win is mostly a stepping stone to a more flexible install, might be worth taking the plunge right if you need the latest updates.

so i just need to go to this path pyblish-win\lib\pyblish\modules\pyblish-base and run git pull origin master yes?
and it will fix my problem about actions?

You can try, but it isn’t designed to work that way. Either wait, or install from individual repos if it is urgent.

Can we change this topic title? It’s somewhat unclear by itself.

Something like: Action on InstancePlugin: “This is a Bug” would be a better fit?.

Sure, why not!