Merge pull request #1201 from ynput/bugfix/fix-publish-plugin-loading2

Fix publish plugin discovery
This commit is contained in:
Ondřej Samohel 2025-03-21 17:24:24 +01:00 committed by GitHub
commit 3d9fa01058
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 35 additions and 21 deletions

View file

@ -1,6 +1,8 @@
"""Tools for working with python modules and classes."""
import os import os
import sys import sys
import types import types
from typing import Optional
import importlib import importlib
import inspect import inspect
import logging import logging
@ -8,13 +10,22 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def import_filepath(filepath, module_name=None): def import_filepath(
filepath: str,
module_name: Optional[str] = None,
sys_module_name: Optional[str] = None) -> types.ModuleType:
"""Import python file as python module. """Import python file as python module.
Args: Args:
filepath (str): Path to python file. filepath (str): Path to python file.
module_name (str): Name of loaded module. Only for Python 3. By default module_name (str): Name of loaded module. Only for Python 3. By default
is filled with filename of filepath. is filled with filename of filepath.
sys_module_name (str): Name of module in `sys.modules` where to store
loaded module. By default is None so module is not added to
`sys.modules`.
Todo (antirotor): We should add the module to the sys.modules always but
we need to be careful about it and test it properly.
""" """
if module_name is None: if module_name is None:
@ -28,6 +39,9 @@ def import_filepath(filepath, module_name=None):
module_loader = importlib.machinery.SourceFileLoader( module_loader = importlib.machinery.SourceFileLoader(
module_name, filepath module_name, filepath
) )
# only add to sys.modules if requested
if sys_module_name:
sys.modules[sys_module_name] = module
module_loader.exec_module(module) module_loader.exec_module(module)
return module return module
@ -126,7 +140,8 @@ def classes_from_module(superclass, module):
return classes return classes
def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None): def import_module_from_dirpath(
dirpath, folder_name, dst_module_name=None):
"""Import passed directory as a python module. """Import passed directory as a python module.
Imported module can be assigned as a child attribute of already loaded Imported module can be assigned as a child attribute of already loaded
@ -193,7 +208,7 @@ def is_func_signature_supported(func, *args, **kwargs):
Notes: Notes:
This does NOT check if the function would work with passed arguments This does NOT check if the function would work with passed arguments
only if they can be passed in. If function have *args, **kwargs only if they can be passed in. If function have *args, **kwargs
in paramaters, this will always return 'True'. in parameters, this will always return 'True'.
Example: Example:
>>> def my_function(my_number): >>> def my_function(my_number):

View file

@ -1,3 +1,5 @@
"""Library functions for publishing."""
from __future__ import annotations
import os import os
import sys import sys
import inspect import inspect
@ -12,8 +14,8 @@ import pyblish.plugin
import pyblish.api import pyblish.api
from ayon_core.lib import ( from ayon_core.lib import (
Logger,
import_filepath, import_filepath,
Logger,
filter_profiles, filter_profiles,
) )
from ayon_core.settings import get_project_settings from ayon_core.settings import get_project_settings
@ -163,7 +165,7 @@ class HelpContent:
def load_help_content_from_filepath(filepath): def load_help_content_from_filepath(filepath):
"""Load help content from xml file. """Load help content from xml file.
Xml file may containt errors and warnings. Xml file may contain errors and warnings.
""" """
errors = {} errors = {}
warnings = {} warnings = {}
@ -208,8 +210,9 @@ def load_help_content_from_plugin(plugin):
return load_help_content_from_filepath(filepath) return load_help_content_from_filepath(filepath)
def publish_plugins_discover(paths=None): def publish_plugins_discover(
"""Find and return available pyblish plug-ins paths: Optional[list[str]] = None) -> DiscoverResult:
"""Find and return available pyblish plug-ins.
Overridden function from `pyblish` module to be able to collect Overridden function from `pyblish` module to be able to collect
crashed files and reason of their crash. crashed files and reason of their crash.
@ -252,17 +255,14 @@ def publish_plugins_discover(paths=None):
continue continue
try: try:
module = import_filepath(abspath, mod_name) module = import_filepath(
abspath, mod_name, sys_module_name=mod_name)
# Store reference to original module, to avoid except Exception as err: # noqa: BLE001
# garbage collection from collecting it's global # we need broad exception to catch all possible errors.
# imports, such as `import os`.
sys.modules[abspath] = module
except Exception as err:
result.crashed_file_paths[abspath] = sys.exc_info() result.crashed_file_paths[abspath] = sys.exc_info()
log.debug("Skipped: \"%s\" (%s)", mod_name, err) log.debug('Skipped: "%s" (%s)', mod_name, err)
continue continue
for plugin in pyblish.plugin.plugins_from_module(module): for plugin in pyblish.plugin.plugins_from_module(module):
@ -280,9 +280,8 @@ def publish_plugins_discover(paths=None):
continue continue
plugin_names.append(plugin.__name__) plugin_names.append(plugin.__name__)
plugin.__file__ = module.__file__
plugin.__module__ = module.__file__ key = f"{module.__file__}.{plugin.__name__}"
key = "{0}.{1}".format(plugin.__module__, plugin.__name__)
plugins[key] = plugin plugins[key] = plugin
# Include plug-ins from registration. # Include plug-ins from registration.
@ -361,7 +360,7 @@ def get_plugin_settings(plugin, project_settings, log, category=None):
# Settings category determined from path # Settings category determined from path
# - usually path is './<category>/plugins/publish/<plugin file>' # - usually path is './<category>/plugins/publish/<plugin file>'
# - category can be host name of addon name ('maya', 'deadline', ...) # - category can be host name of addon name ('maya', 'deadline', ...)
filepath = os.path.normpath(inspect.getsourcefile(plugin)) filepath = os.path.normpath(inspect.getfile(plugin))
split_path = filepath.rsplit(os.path.sep, 5) split_path = filepath.rsplit(os.path.sep, 5)
if len(split_path) < 4: if len(split_path) < 4:
@ -427,7 +426,7 @@ def filter_pyblish_plugins(plugins):
log = Logger.get_logger("filter_pyblish_plugins") log = Logger.get_logger("filter_pyblish_plugins")
# TODO: Don't use host from 'pyblish.api' but from defined host by us. # TODO: Don't use host from 'pyblish.api' but from defined host by us.
# - kept becau on farm is probably used host 'shell' which propably # - kept because on farm is probably used host 'shell' which probably
# affect how settings are applied there # affect how settings are applied there
host_name = pyblish.api.current_host() host_name = pyblish.api.current_host()
project_name = os.environ.get("AYON_PROJECT_NAME") project_name = os.environ.get("AYON_PROJECT_NAME")
@ -529,7 +528,7 @@ def filter_instances_for_context_plugin(plugin, context):
Args: Args:
plugin (pyblish.api.Plugin): Plugin with filters. plugin (pyblish.api.Plugin): Plugin with filters.
context (pyblish.api.Context): Pyblish context with insances. context (pyblish.api.Context): Pyblish context with instances.
Returns: Returns:
Iterator[pyblish.lib.Instance]: Iteration of valid instances. Iterator[pyblish.lib.Instance]: Iteration of valid instances.