Dependency Inject Host


#1


# NOTE: THIS FEATURE HAS BEEN DEPRECATED!

I know this is a little ahead as you haven’t been given a chance to play with DI yet, but for future discussion, I’ll pop this in here hoping there will be a time where we can have a discussion about it’s pros/cons.

Goal

To facilitate testing and to further decouple plug-ins from their environment by eliminating host-bound imports and enable imports of host-bound plug-ins from the outside and/or other hosts.

Implementation

It would look like this.

import pyblish.api

class SelectCharacters(pyblish.api.Selector):
  def process(self, context, cmds):
    for char in cmds.ls("*_char"):
      context.create_instance(char)

In which case cmds is a reference to maya.cmds and is injected upon integration via the Maya extension.

userSetup.py

from maya import cmds
import pyblish.api
pyblish.api.register_service("cmds", cmds)

Still, the above is merely moving the import statement into the method signature, it’s not terribly unique. What is unique however is how we can design plug-ins to remain independent of a host even when it requires the use of host-specific modules, such as maya.cmds.

import pyblish.api

class SelectCharacters(pyblish.api.Selector):
  def process(self, context, host):
    for char in host.ls("*_char"):
      context.create_instance(char)

In which case host represents maya.cmds when the plug-in is used in the context of Maya, and nuke when in Nuke etc.

The question then is, “won’t their interfaces differ?” and you are absolutely right. Maya will provide functions that differ from Nuke and Houdini and vice versa so it is unlikely that the feature will work everywhere.

But it might work in special and more important areas where reuse is more important.

To provide plug-ins with a common interface towards whatever host it supports, you do what you would normally do when you have multiple conflicting APIs that needs coordination; wrap them up into a common shared interface.

maya

from maya import cmds

class Host(object):
  def list_items(self, query):
    return cmds.ls(query)

import pyblish.api
pyblish.api.register_service("host", Host())

nuke

import nuke

class Host(object):
  def list_items(self, query):
    return nuke.allNodes(query)

import pyblish.api
pyblish.api.register_service("host", Host())

The end result is the possibility to now write a plug-in that works on-top of this interface.

import pyblish.api

class SelectCharacters(pyblish.api.Selector):
  def process(self, context, host):
    for char in host.list_items("*_char"):
      context.create_instance(char)

Discussion

This is something you’ll be able to do right away upon the release of 1.1, but what this proposal is about is whether or not to make this a default, recommended behaviour.

The question is, is it worth pursuing, what advantage(s) does it provide, if any, and at what cost?


Learning Pyblish by Example
#2

To be honest I don’t see many advantages in this, at least not compared to the amount of complexity this introduces.

For instance I’m currently running a few plugins that are multi host and I’m dealing with it in a very simple way. For example here is simplified version of my scene selector

import pyblish.api

@pyblish.api.log
class SelectWorkfile(pyblish.api.Selector):
    host = sys.executable.lower()
    def process_context(self, context):
        if "nuke" in self.host:
            current_file = self.process_nuke()
        elif "maya" in self.host:
            current_file = self.process_maya()
        elif "houdini" in self.host:
            current_file = self.process_houdini()
        else:
            current_file = None
            self.log.warning('Workfile selection in current host is not supported yet!')
        if current_file:
            instance = context.create_instance(name=filename)
            instance.set_data('family', value='workFile')
            instance.set_data("path", value=current_file)
            instance.add(current_file)

   # NUKE
    def process_nuke(self):
        import nuke
        return nuke.root().name()
    # MAYA
    def process_maya(self):
        from maya import cmds
        return cmds.file(q=True, location=True)
    # HOUDINI
    def process_houdini(self):
        import hou
        return hou.hipFile.path()

Very simple and does the job. Adding another host is a question of 6 lines of code.

You proposal can potentially be very very powerful, but as default I’d keep it simple.


#3

About complexity, here’s what the equivalent would look like with DI.

import pyblish.api

@pyblish.api.log
class SelectWorkfile(pyblish.api.Selector):
  hosts = ["maya", "nuke", "houdini"]
  def process(self, context, host):
    current_file = host.current_file()
    instance = context.create_instance(name=filename)
    instance.set_data('family', value='workFile')
    instance.set_data("path", value=current_file)
    instance.add(current_file)

For which a host-dependent implementation is provided per supported host.

A Maya implementation might then look like this and would be provided separately, either by you or another TD.

from maya import cmds

class Host(object):
  def current_file(self):
    return cmds.file(q=True, location=True)

import pyblish.api
pyblish.api.register_service("host", Host())

On that note, it also means increased distribution of responsibility between developers within an organisation, as some can develop plug-ins, and others provide an API for them, based on their needs.

I understand it may seem complex at the moment, but suspect that as we get more familiar with DI as I have whilst implementing and testing it, it’ll grow on us. :slight_smile:


#4

On another note (that’s why I love bring it up for discussion like this, gets your mind turning) it also means it’s a potential alternative to the otherwise default data members provided by an integration, such as currentFile.

class MySelector(...):
  def process(self, context):
    assert "marcus" in context.data("currentFile"), "Sorry, you did wrong"

By having each integration provide a skeleton version of Host that could later be sub-classed and extended, we could bridge only the most common properties, such as the currently opened file, and let others append what they or their plug-ins require.

class MySelector(...):
  def process(self, context, host):
    assert "marcus" in host.current_file(), "Sorry, you did wrong"

Default skeleton

from maya import cmds

class DefaultHost(object):
  def current_file(self):
    return cmds.file(q=True, location=True)

Subclassed skeleton

import our_special_library
import pyblish_maya

class DefaultHost(pyblish_maya.DefaultHost):
  def our_special_function(self):
    return our_special_library.some_value()

#5

Just wanted to chime in with my scene selector;

import os
import shutil

import pyblish.api


@pyblish.api.log
class SelectScene(pyblish.api.Selector):
    """"""

    order = pyblish.api.Selector.order + 0.1
    hosts = ['*']
    version = (0, 1, 0)

    def process_context(self, context):

        current_file = context.data('currentFile')
        current_dir = os.path.dirname(current_file)
        publish_dir = os.path.join(current_dir, 'publish')
        publish_file = os.path.join(publish_dir, os.path.basename(current_file))

        # create instance
        instance = context.create_instance(name=os.path.basename(current_file))

        instance.set_data('family', value='scene')
        instance.set_data('workPath', value=current_file)
        instance.set_data('publishPath', value=publish_file)

        # deadline data
        instance.context.set_data('deadlineInput', value=publish_file)

        # ftrack data
        components = {'publish_file': {'path': publish_file}}

        if pyblish.api.current_host() == 'nuke':
            components['nukescript'] = {'path': current_file}
        else:
            components['work_file'] = {'path': current_file}

        instance.set_data('ftrackComponents', value=components)

Offset the selector to take advantage of the currentFile default data member. Scene selector for all hosts.


#6

This is nice, I’ll steal the idea. :wink: