diff --git a/changelog.md b/changelog.md index 159ff0baeb..46cceb9fdc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,32 @@ # Pype changelog # Welcome to pype changelog -## 2.1 ## +## 2.2.0 ## +_release date: 8 Sept 2019_ + +**new**: +- _(pype)_ add customisable workflow for creating quicktimes from renders or playblasts +- _(nuke)_ option to choose deadline chunk size on write nodes +- _(nukestudio)_ added option to publish soft effects (subTrackItems) from NukeStudio as subsets including LUT files. these can then be loaded in nuke or NukeStudio +- _(nuke)_ option to build nuke script from previously published latest versions of plate and render subsets. +- _(nuke)_ nuke writes now have deadline tab. +- _(ftrack)_ Prepare Project action can now be used for creating the base folder structure on disk and in ftrack, setting up all the initial project attributes and it automatically prepares `pype_project_config` folder for the given project. +- _(clockify)_ Added support for time tracking in clockify. This currently in addition to ftrack time logs, but does not completely replace them. +- _(pype)_ any attributes in Creator and Loader plugins can now be customised using pype preset system + +**changed**: +- nukestudio now uses workio API for workfiles +- _(maya)_ "FIX FPS" prompt in maya now appears in the middle of the screen +- _(muster)_ can now be configured with custom templates +- _(pype)_ global publishing plugins can now be configured using presets as well as host specific ones + + +**fix**: +- wrong version retrieval from path in certain scenarios +- nuke reset resolution wasn't working in certain scenarios + +## 2.1.0 ## +_release date: 6 Aug 2019_ A large cleanup release. Most of the change are under the hood. diff --git a/pype/__init__.py b/pype/__init__.py index a5858f49e7..bcbedc9a90 100644 --- a/pype/__init__.py +++ b/pype/__init__.py @@ -3,6 +3,8 @@ import os from pyblish import api as pyblish from avalon import api as avalon from .lib import filter_pyblish_plugins +from pypeapp import config + import logging log = logging.getLogger(__name__) @@ -16,6 +18,51 @@ PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "global", "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "global", "load") +# we are monkey patching `avalon.api.discover()` to allow us to load +# plugin presets on plugins being discovered by avalon. Little bit of +# hacking, but it allows us to add out own features without need +# to modify upstream code. + +_original_discover = avalon.discover + + +def patched_discover(superclass): + """ + Monkey patched version of :func:`avalon.api.discover()`. It allows + us to load presets on plugins being discovered. + """ + # run original discover and get plugins + plugins = _original_discover(superclass) + + # determine host application to use for finding presets + host = avalon.registered_host().__name__.split(".")[-1] + + # map plugin superclass to preset json. Currenly suppoted is load and + # create (avalon.api.Loader and avalon.api.Creator) + plugin_type = "undefined" + if superclass.__name__.split(".")[-1] == "Loader": + plugin_type = "load" + elif superclass.__name__.split(".")[-1] == "Creator": + plugin_type = "create" + + print(">>> trying to find presets for {}:{} ...".format(host, plugin_type)) + try: + config_data = config.get_presets()['plugins'][host][plugin_type] + except KeyError: + print("*** no presets found.") + else: + for plugin in plugins: + if plugin.__name__ in config_data: + print(">>> We have preset for {}".format(plugin.__name__)) + for option, value in config_data[plugin.__name__].items(): + if option == "enabled" and value is False: + setattr(plugin, "active", False) + print(" - is disabled by preset") + else: + setattr(plugin, option, value) + print(" - setting `{}`: `{}`".format(option, value)) + return plugins + def install(): log.info("Registering global plug-ins..") @@ -23,6 +70,9 @@ def install(): pyblish.register_discovery_filter(filter_pyblish_plugins) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) + # apply monkey patched discover to original one + avalon.discover = patched_discover + def uninstall(): log.info("Deregistering global plug-ins..") @@ -30,3 +80,6 @@ def uninstall(): pyblish.deregister_discovery_filter(filter_pyblish_plugins) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) log.info("Global plug-ins unregistred") + + # restore original discover + avalon.discover = _original_discover diff --git a/pype/clockify/clockify.py b/pype/clockify/clockify.py index ed6d996e2e..5e6cfec778 100644 --- a/pype/clockify/clockify.py +++ b/pype/clockify/clockify.py @@ -194,7 +194,7 @@ class ClockifyModule: self.message_widget = MessageWidget( self.main_parent, msg, "Clockify - Info Message" ) - self.message_widget.closed.connect(self.message_widget) + self.message_widget.closed.connect(self.on_message_widget_close) self.message_widget.show() return diff --git a/pype/lib.py b/pype/lib.py index 6a7f274169..6f6895085e 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -5,6 +5,7 @@ import importlib import itertools import contextlib import subprocess +import inspect from .vendor import pather from .vendor.pather.error import ParseError @@ -469,9 +470,7 @@ def filter_pyblish_plugins(plugins): host = api.current_host() - presets = config.get_presets().get('plugins', {}).get(host, {}).get( - "publish", {} - ) + presets = config.get_presets().get('plugins', {}) # iterate over plugins for plugin in plugins[:]: @@ -479,10 +478,20 @@ def filter_pyblish_plugins(plugins): if not presets: continue + file = os.path.normpath(inspect.getsourcefile(plugin)) + file = os.path.normpath(file) + + # host determined from path + host_from_file = file.split(os.path.sep)[-3:-2][0] + plugin_kind = file.split(os.path.sep)[-2:-1][0] + try: - config_data = presets[plugin.__name__] # noqa: E501 + config_data = presets[host]["publish"][plugin.__name__] except KeyError: - continue + try: + config_data = presets[host_from_file][plugin_kind][plugin.__name__] # noqa: E501 + except KeyError: + continue for option, value in config_data.items(): if option == "enabled" and value is False: diff --git a/pype/plugin.py b/pype/plugin.py index c77b9927e1..a3460e693e 100644 --- a/pype/plugin.py +++ b/pype/plugin.py @@ -2,13 +2,54 @@ import tempfile import os import pyblish.api +from pypeapp import config +import inspect + ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 -class Extractor(pyblish.api.InstancePlugin): +def imprint_attributes(plugin): + """ + Load presets by class and set them as attributes (if found) + + :param plugin: plugin instance + :type plugin: instance + """ + file = inspect.getfile(plugin.__class__) + file = os.path.normpath(file) + plugin_kind = file.split(os.path.sep)[-2:-1][0] + plugin_host = file.split(os.path.sep)[-3:-2][0] + plugin_name = type(plugin).__name__ + try: + config_data = config.get_presets()['plugins'][plugin_host][plugin_kind][plugin_name] # noqa: E501 + except KeyError: + print("preset not found") + return + + for option, value in config_data.items(): + if option == "enabled" and value is False: + setattr(plugin, "active", False) + else: + setattr(plugin, option, value) + print("setting {}: {} on {}".format(option, value, plugin_name)) + + +class ContextPlugin(pyblish.api.ContextPlugin): + def process(cls, *args, **kwargs): + imprint_attributes(cls) + super(ContextPlugin, cls).process(cls, *args, **kwargs) + + +class InstancePlugin(pyblish.api.InstancePlugin): + def process(cls, *args, **kwargs): + imprint_attributes(cls) + super(ContextPlugin, cls).process(cls, *args, **kwargs) + + +class Extractor(InstancePlugin): """Extractor base class. The extractor base class implements a "staging_dir" function used to diff --git a/pype/tests/test_avalon_plugin_presets.py b/pype/tests/test_avalon_plugin_presets.py new file mode 100644 index 0000000000..7f023ea358 --- /dev/null +++ b/pype/tests/test_avalon_plugin_presets.py @@ -0,0 +1,40 @@ +import avalon.api as api +import pype + + +class MyTestCreator(api.Creator): + + my_test_property = "A" + + def __init__(self, name, asset, options=None, data=None): + super(MyTestCreator, self).__init__(self, name, asset, + options=None, data=None) + + +# this is hack like no other - we need to inject our own avalon host +# and bypass all its validation. Avalon hosts are modules that needs +# `ls` callable as attribute. Voila: +class Test: + __name__ = "test" + ls = len + + def __call__(self): + pass + + +def test_avalon_plugin_presets(monkeypatch, printer): + + pype.install() + api.register_host(Test()) + api.register_plugin(api.Creator, MyTestCreator) + plugins = api.discover(api.Creator) + printer("Test if we got our test plugin") + assert MyTestCreator in plugins + for p in plugins: + if p.__name__ == "MyTestCreator": + printer("Test if we have overriden existing property") + assert p.my_test_property == "B" + printer("Test if we have overriden superclass property") + assert p.active is False + printer("Test if we have added new property") + assert p.new_property == "new" diff --git a/pype/tests/test_pyblish_filter.py b/pype/tests/test_pyblish_filter.py index 8d747e63df..cf3d5d6015 100644 --- a/pype/tests/test_pyblish_filter.py +++ b/pype/tests/test_pyblish_filter.py @@ -18,7 +18,7 @@ def test_pyblish_plugin_filter_modifier(printer, monkeypatch): assert len(plugins) == 0 paths = pyblish.api.registered_paths() printer("Test if we have no registered plugin paths") - print(paths) + assert len(paths) == 0 class MyTestPlugin(pyblish.api.InstancePlugin): my_test_property = 1