diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 36252c9f3d..72f3774e1a 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -36,6 +36,9 @@ from .lib import ( context_plugin_should_run, get_instance_staging_dir, get_publish_repre_path, + + apply_plugin_settings_automatically, + get_plugin_settings, ) from .abstract_expected_files import ExpectedFiles @@ -80,6 +83,9 @@ __all__ = ( "get_instance_staging_dir", "get_publish_repre_path", + "apply_plugin_settings_automatically", + "get_plugin_settings", + "ExpectedFiles", "RenderInstance", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 8b6212b3ef..080f93e514 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -355,29 +355,55 @@ def publish_plugins_discover(paths=None): return result -def _get_plugin_settings(host_name, project_settings, plugin, log): +def get_plugin_settings(plugin, project_settings, log, category=None): """Get plugin settings based on host name and plugin name. + Note: + Default implementation of automated settings is passing host name + into 'category'. + Args: - host_name (str): Name of host. + plugin (pyblish.Plugin): Plugin where settings are applied. project_settings (dict[str, Any]): Project settings. - plugin (pyliblish.Plugin): Plugin where settings are applied. log (logging.Logger): Logger to log messages. + category (Optional[str]): Settings category key where to look + for plugin settings. Returns: dict[str, Any]: Plugin settings {'attribute': 'value'}. """ - # Use project settings from host name category when available - try: - return ( - project_settings - [host_name] - ["publish"] - [plugin.__name__] - ) - except KeyError: - pass + # Plugin can define settings category by class attribute + # - it's impossible to set `settings_category` via settings because + # obviously settings are not applied before it. + # - if `settings_category` is set the fallback category method is ignored + settings_category = getattr(plugin, "settings_category", None) + if settings_category: + try: + return ( + project_settings + [settings_category] + ["publish"] + [plugin.__name__] + ) + except KeyError: + log.warning(( + "Couldn't find plugin '{}' settings" + " under settings category '{}'" + ).format(plugin.__name__, settings_category)) + return {} + + # Use project settings based on a category name + if category: + try: + return ( + project_settings + [category] + ["publish"] + [plugin.__name__] + ) + except KeyError: + pass # Settings category determined from path # - usually path is './/plugins/publish/' @@ -386,9 +412,10 @@ def _get_plugin_settings(host_name, project_settings, plugin, log): split_path = filepath.rsplit(os.path.sep, 5) if len(split_path) < 4: - log.warning( - 'plugin path too short to extract host {}'.format(filepath) - ) + log.debug(( + "Plugin path is too short to automatically" + " extract settings category. {}" + ).format(filepath)) return {} category_from_file = split_path[-4] @@ -410,6 +437,28 @@ def _get_plugin_settings(host_name, project_settings, plugin, log): return {} +def apply_plugin_settings_automatically(plugin, settings, logger=None): + """Automatically apply plugin settings to a plugin object. + + Note: + This function was created to be able to use it in custom overrides of + 'apply_settings' class method. + + Args: + plugin (type[pyblish.api.Plugin]): Class of a plugin. + settings (dict[str, Any]): Plugin specific settings. + logger (Optional[logging.Logger]): Logger to log debug messages about + applied settings values. + """ + + for option, value in settings.items(): + if logger: + logger.debug("Plugin {} - Attr: {} -> {}".format( + option, value, plugin.__name__ + )) + setattr(plugin, option, value) + + def filter_pyblish_plugins(plugins): """Pyblish plugin filter which applies OpenPype settings. @@ -453,13 +502,10 @@ def filter_pyblish_plugins(plugins): ) else: # Automated - plugin_settins = _get_plugin_settings( - host_name, project_settings, plugin, log + plugin_settins = get_plugin_settings( + plugin, project_settings, log, host_name ) - for option, value in plugin_settins.items(): - log.info("setting {}:{} on plugin {}".format( - option, value, plugin.__name__)) - setattr(plugin, option, value) + apply_plugin_settings_automatically(plugin, plugin_settins, log) # Remove disabled plugins if getattr(plugin, "enabled", True) is False: diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 2c57537223..3ef6272373 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -506,6 +506,67 @@ or the scene file was copy pasted from different context. #### *Known errors* When there is a known error that can't be fixed by the user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raised. The only difference is that its message is shown in UI to the artist otherwise a neutral message without context is shown. +### Plugins +Plugin is a single processing unit that can work with publish context and instances. + +#### Plugin types +There are 2 types of plugins - `InstancePlugin` and `ContextPlugin`. Be aware that inheritance of plugin from `InstancePlugin` or `ContextPlugin` actually does not affect if plugin is instance or context plugin, that is affected by argument name in `process` method. + +```python +import pyblish.api + + +# Context plugin +class MyContextPlugin(pyblish.api.ContextPlugin): + def process(self, context): + ... + +# Instance plugin +class MyInstancePlugin(pyblish.api.InstancePlugin): + def process(self, instance): + ... + +# Still an instance plugin +class MyOtherInstancePlugin(pyblish.api.ContextPlugin): + def process(self, instance): + ... +``` + +#### Plugin filtering +By pyblish logic, plugins have predefined filtering class attributes `hosts`, `targets` and `families`. Filter by `hosts` and `targets` are filters that are applied for current publishing process. Both filters are registered in `pyblish` module, `hosts` filtering may not match OpenPype host name (e.g. farm publishing uses `shell` in pyblish). Filter `families` works only on instance plugins and is dynamic during publish process by changing families of an instance. + +All filters are list of a strings `families = ["image"]`. Empty list is invalid filter and plugin will be skipped, to allow plugin for all values use a start `families = ["*"]`. For more detailed filtering options check [pyblish documentation](https://api.pyblish.com/pluginsystem). + +Each plugin must have order, there are 4 order milestones - Collect, Validate, Extract, Integration. Any plugin below collection order won't be processed. for more details check [pyblish documentation](https://api.pyblish.com/ordering). + +#### Plugin settings +Pyblish plugins may have settings. There are 2 ways how settings are applied, first is automated, and it's logic is based on function `filter_pyblish_plugins` in `./openpype/pipeline/publish/lib.py`, second is explicit by implementing class method `apply_settings` on a plugin. + + +Automated logic is expecting specific structure of project settings `project_settings[{category}]["plugins"]["publish"][{plugin class name}]`. The category is a key in root of project settings. There are currently 3 ways how the category key is received. +1. Use `settings_category` class attribute value from plugin. If `settings_category` is not `None` there is not any fallback to other way. +2. Use currently registered pyblish host. This will be probably deprecated soon. +3. Use 3rd folder name from a plugin filepath. From path `./maya/plugins/publish/collect_render.py` is used `maya` as the key. + +For any other use-case is recommended to use explicit approach by implementing `apply_settings` method. Must use `@classmethod` decorator and expect arguments for project settings and system settings. We're planning to support single argument with only project settings. +```python +import pyblish.api + + +class MyPlugin(pyblish.api.InstancePlugin): + profiles = [] + + @classmethod + def apply_settings(cls, project_settings, system_settings): + cls.profiles = ( + project_settings + ["addon"] + ["plugins"] + ["publish"] + ["vfx_profiles"] + ) +``` + ### Plugin extension Publish plugins can be extended by additional logic when inheriting from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of the most important usages is to be able turn on/off optional plugins. @@ -596,4 +657,4 @@ Publish attributes work the same way as create attributes but the source of attr ### Create dialog ![Publisher UI - Create dialog](assets/publisher_create_dialog.png) -Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator. \ No newline at end of file +Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator.