Merge branch 'develop' into enhancement/maya_validate_model_content

This commit is contained in:
Roy Nieterau 2024-04-04 15:58:46 +02:00 committed by GitHub
commit bbaa5c9758
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
312 changed files with 5257 additions and 4008 deletions

View file

@ -14,3 +14,15 @@ AYON_SERVER_ENABLED = True
# Indicate if AYON entities should be used instead of OpenPype entities
USE_AYON_ENTITIES = True
# -------------------------
__all__ = (
"__version__",
# Deprecated
"AYON_CORE_ROOT",
"PACKAGE_DIR",
"PLUGINS_DIR",
"AYON_SERVER_ENABLED",
"USE_AYON_ENTITIES",
)

View file

@ -16,6 +16,7 @@ import six
import appdirs
import ayon_api
from ayon_core import AYON_CORE_ROOT
from ayon_core.lib import Logger, is_dev_mode_enabled
from ayon_core.settings import get_studio_settings
@ -335,14 +336,70 @@ def _load_ayon_addons(openpype_modules, modules_key, log):
return addons_to_skip_in_core
def _load_ayon_core_addons_dir(
ignore_addon_names, openpype_modules, modules_key, log
):
addons_dir = os.path.join(AYON_CORE_ROOT, "addons")
if not os.path.exists(addons_dir):
return
imported_modules = []
# Make sure that addons which already have client code are not loaded
# from core again, with older code
filtered_paths = []
for name in os.listdir(addons_dir):
if name in ignore_addon_names:
continue
path = os.path.join(addons_dir, name)
if os.path.isdir(path):
filtered_paths.append(path)
for path in filtered_paths:
while path in sys.path:
sys.path.remove(path)
sys.path.insert(0, path)
for name in os.listdir(path):
fullpath = os.path.join(path, name)
if os.path.isfile(fullpath):
basename, ext = os.path.splitext(name)
if ext != ".py":
continue
else:
basename = name
try:
module = __import__(basename, fromlist=("",))
for attr_name in dir(module):
attr = getattr(module, attr_name)
if (
inspect.isclass(attr)
and issubclass(attr, AYONAddon)
):
new_import_str = "{}.{}".format(modules_key, basename)
sys.modules[new_import_str] = module
setattr(openpype_modules, basename, module)
imported_modules.append(module)
break
except Exception:
log.error(
"Failed to import addon '{}'.".format(fullpath),
exc_info=True
)
return imported_modules
def _load_addons_in_core(
ignore_addon_names, openpype_modules, modules_key, log
):
_load_ayon_core_addons_dir(
ignore_addon_names, openpype_modules, modules_key, log
)
# Add current directory at first place
# - has small differences in import logic
current_dir = os.path.abspath(os.path.dirname(__file__))
hosts_dir = os.path.join(os.path.dirname(current_dir), "hosts")
modules_dir = os.path.join(os.path.dirname(current_dir), "modules")
hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts")
modules_dir = os.path.join(AYON_CORE_ROOT, "modules")
ignored_host_names = set(IGNORED_HOSTS_IN_AYON)
ignored_module_dir_filenames = (
@ -1075,7 +1132,7 @@ class AddonsManager:
"""Print out report of time spent on addons initialization parts.
Reporting is not automated must be implemented for each initialization
part separatelly. Reports must be stored to `_report` attribute.
part separately. Reports must be stored to `_report` attribute.
Print is skipped if `_report` is empty.
Attribute `_report` is dictionary where key is "label" describing
@ -1267,7 +1324,7 @@ class TrayAddonsManager(AddonsManager):
def add_doubleclick_callback(self, addon, callback):
"""Register doubleclick callbacks on tray icon.
Currently there is no way how to determine which is launched. Name of
Currently, there is no way how to determine which is launched. Name of
callback can be defined with `doubleclick_callback` attribute.
Missing feature how to define default callback.

View file

@ -0,0 +1,58 @@
from .constants import (
APPLICATIONS_ADDON_ROOT,
DEFAULT_ENV_SUBGROUP,
PLATFORM_NAMES,
)
from .exceptions import (
ApplicationNotFound,
ApplicationExecutableNotFound,
ApplicationLaunchFailed,
MissingRequiredKey,
)
from .defs import (
LaunchTypes,
ApplicationExecutable,
UndefinedApplicationExecutable,
ApplicationGroup,
Application,
EnvironmentToolGroup,
EnvironmentTool,
)
from .hooks import (
LaunchHook,
PreLaunchHook,
PostLaunchHook,
)
from .manager import (
ApplicationManager,
ApplicationLaunchContext,
)
from .addon import ApplicationsAddon
__all__ = (
"DEFAULT_ENV_SUBGROUP",
"PLATFORM_NAMES",
"ApplicationNotFound",
"ApplicationExecutableNotFound",
"ApplicationLaunchFailed",
"MissingRequiredKey",
"LaunchTypes",
"ApplicationExecutable",
"UndefinedApplicationExecutable",
"ApplicationGroup",
"Application",
"EnvironmentToolGroup",
"EnvironmentTool",
"LaunchHook",
"PreLaunchHook",
"PostLaunchHook",
"ApplicationManager",
"ApplicationLaunchContext",
"ApplicationsAddon",
)

View file

@ -0,0 +1,173 @@
import os
import json
from ayon_core.addon import AYONAddon, IPluginPaths, click_wrap
from .constants import APPLICATIONS_ADDON_ROOT
from .defs import LaunchTypes
from .manager import ApplicationManager
class ApplicationsAddon(AYONAddon, IPluginPaths):
name = "applications"
def initialize(self, settings):
# TODO remove when addon is removed from ayon-core
self.enabled = self.name in settings
def get_app_environments_for_context(
self,
project_name,
folder_path,
task_name,
full_app_name,
env_group=None,
launch_type=None,
env=None,
):
"""Calculate environment variables for launch context.
Args:
project_name (str): Project name.
folder_path (str): Folder path.
task_name (str): Task name.
full_app_name (str): Full application name.
env_group (Optional[str]): Environment group.
launch_type (Optional[str]): Launch type.
env (Optional[dict[str, str]]): Environment variables to update.
Returns:
dict[str, str]: Environment variables for context.
"""
from ayon_applications.utils import get_app_environments_for_context
if not full_app_name:
return {}
return get_app_environments_for_context(
project_name,
folder_path,
task_name,
full_app_name,
env_group=env_group,
launch_type=launch_type,
env=env,
addons_manager=self.manager
)
def get_farm_publish_environment_variables(
self,
project_name,
folder_path,
task_name,
full_app_name=None,
env_group=None,
):
"""Calculate environment variables for farm publish.
Args:
project_name (str): Project name.
folder_path (str): Folder path.
task_name (str): Task name.
env_group (Optional[str]): Environment group.
full_app_name (Optional[str]): Full application name. Value from
environment variable 'AYON_APP_NAME' is used if 'None' is
passed.
Returns:
dict[str, str]: Environment variables for farm publish.
"""
if full_app_name is None:
full_app_name = os.getenv("AYON_APP_NAME")
return self.get_app_environments_for_context(
project_name,
folder_path,
task_name,
full_app_name,
env_group=env_group,
launch_type=LaunchTypes.farm_publish
)
def get_applications_manager(self, settings=None):
"""Get applications manager.
Args:
settings (Optional[dict]): Studio/project settings.
Returns:
ApplicationManager: Applications manager.
"""
return ApplicationManager(settings)
def get_plugin_paths(self):
return {
"publish": [
os.path.join(APPLICATIONS_ADDON_ROOT, "plugins", "publish")
]
}
# --- CLI ---
def cli(self, addon_click_group):
main_group = click_wrap.group(
self._cli_main, name=self.name, help="Applications addon"
)
(
main_group.command(
self._cli_extract_environments,
name="extractenvironments",
help=(
"Extract environment variables for context into json file"
)
)
.argument("output_json_path")
.option("--project", help="Project name", default=None)
.option("--folder", help="Folder path", default=None)
.option("--task", help="Task name", default=None)
.option("--app", help="Application name", default=None)
.option(
"--envgroup",
help="Environment group (e.g. \"farm\")",
default=None
)
)
# Convert main command to click object and add it to parent group
addon_click_group.add_command(
main_group.to_click_obj()
)
def _cli_main(self):
pass
def _cli_extract_environments(
self, output_json_path, project, folder, task, app, envgroup
):
"""Produces json file with environment based on project and app.
Called by farm integration to propagate environment into farm jobs.
Args:
output_json_path (str): Output json file path.
project (str): Project name.
folder (str): Folder path.
task (str): Task name.
app (str): Full application name e.g. 'maya/2024'.
envgroup (str): Environment group.
"""
if all((project, folder, task, app)):
env = self.get_farm_publish_environment_variables(
project, folder, task, app, env_group=envgroup,
)
else:
env = os.environ.copy()
output_dir = os.path.dirname(output_json_path)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
with open(output_json_path, "w") as file_stream:
json.dump(env, file_stream, indent=4)

View file

@ -0,0 +1,6 @@
import os
APPLICATIONS_ADDON_ROOT = os.path.dirname(os.path.abspath(__file__))
PLATFORM_NAMES = {"windows", "linux", "darwin"}
DEFAULT_ENV_SUBGROUP = "standard"

View file

@ -0,0 +1,404 @@
import os
import platform
import json
import copy
from ayon_core.lib import find_executable
class LaunchTypes:
"""Launch types are filters for pre/post-launch hooks.
Please use these variables in case they'll change values.
"""
# Local launch - application is launched on local machine
local = "local"
# Farm render job - application is on farm
farm_render = "farm-render"
# Farm publish job - integration post-render job
farm_publish = "farm-publish"
# Remote launch - application is launched on remote machine from which
# can be started publishing
remote = "remote"
# Automated launch - application is launched with automated publishing
automated = "automated"
class ApplicationExecutable:
"""Representation of executable loaded from settings."""
def __init__(self, executable):
# Try to format executable with environments
try:
executable = executable.format(**os.environ)
except Exception:
pass
# On MacOS check if exists path to executable when ends with `.app`
# - it is common that path will lead to "/Applications/Blender" but
# real path is "/Applications/Blender.app"
if platform.system().lower() == "darwin":
executable = self.macos_executable_prep(executable)
self.executable_path = executable
def __str__(self):
return self.executable_path
def __repr__(self):
return "<{}> {}".format(self.__class__.__name__, self.executable_path)
@staticmethod
def macos_executable_prep(executable):
"""Try to find full path to executable file.
Real executable is stored in '*.app/Contents/MacOS/<executable>'.
Having path to '*.app' gives ability to read it's plist info and
use "CFBundleExecutable" key from plist to know what is "executable."
Plist is stored in '*.app/Contents/Info.plist'.
This is because some '*.app' directories don't have same permissions
as real executable.
"""
# Try to find if there is `.app` file
if not os.path.exists(executable):
_executable = executable + ".app"
if os.path.exists(_executable):
executable = _executable
# Try to find real executable if executable has `Contents` subfolder
contents_dir = os.path.join(executable, "Contents")
if os.path.exists(contents_dir):
executable_filename = None
# Load plist file and check for bundle executable
plist_filepath = os.path.join(contents_dir, "Info.plist")
if os.path.exists(plist_filepath):
import plistlib
if hasattr(plistlib, "load"):
with open(plist_filepath, "rb") as stream:
parsed_plist = plistlib.load(stream)
else:
parsed_plist = plistlib.readPlist(plist_filepath)
executable_filename = parsed_plist.get("CFBundleExecutable")
if executable_filename:
executable = os.path.join(
contents_dir, "MacOS", executable_filename
)
return executable
def as_args(self):
return [self.executable_path]
def _realpath(self):
"""Check if path is valid executable path."""
# Check for executable in PATH
result = find_executable(self.executable_path)
if result is not None:
return result
# This is not 100% validation but it is better than remove ability to
# launch .bat, .sh or extentionless files
if os.path.exists(self.executable_path):
return self.executable_path
return None
def exists(self):
if not self.executable_path:
return False
return bool(self._realpath())
class UndefinedApplicationExecutable(ApplicationExecutable):
"""Some applications do not require executable path from settings.
In that case this class is used to "fake" existing executable.
"""
def __init__(self):
pass
def __str__(self):
return self.__class__.__name__
def __repr__(self):
return "<{}>".format(self.__class__.__name__)
def as_args(self):
return []
def exists(self):
return True
class ApplicationGroup:
"""Hold information about application group.
Application group wraps different versions(variants) of application.
e.g. "maya" is group and "maya_2020" is variant.
Group hold `host_name` which is implementation name used in AYON. Also
holds `enabled` if whole app group is enabled or `icon` for application
icon path in resources.
Group has also `environment` which hold same environments for all variants.
Args:
name (str): Groups' name.
data (dict): Group defying data loaded from settings.
manager (ApplicationManager): Manager that created the group.
"""
def __init__(self, name, data, manager):
self.name = name
self.manager = manager
self._data = data
self.enabled = data["enabled"]
self.label = data["label"] or None
self.icon = data["icon"] or None
env = {}
try:
env = json.loads(data["environment"])
except Exception:
pass
self._environment = env
host_name = data["host_name"] or None
self.is_host = host_name is not None
self.host_name = host_name
settings_variants = data["variants"]
variants = {}
for variant_data in settings_variants:
app_variant = Application(variant_data, self)
variants[app_variant.name] = app_variant
self.variants = variants
def __repr__(self):
return "<{}> - {}".format(self.__class__.__name__, self.name)
def __iter__(self):
for variant in self.variants.values():
yield variant
@property
def environment(self):
return copy.deepcopy(self._environment)
class Application:
"""Hold information about application.
Object by itself does nothing special.
Args:
data (dict): Data for the version containing information about
executables, variant label or if is enabled.
Only required key is `executables`.
group (ApplicationGroup): App group object that created the application
and under which application belongs.
"""
def __init__(self, data, group):
self._data = data
name = data["name"]
label = data["label"] or name
enabled = False
if group.enabled:
enabled = data.get("enabled", True)
if group.label:
full_label = " ".join((group.label, label))
else:
full_label = label
env = {}
try:
env = json.loads(data["environment"])
except Exception:
pass
arguments = data["arguments"]
if isinstance(arguments, dict):
arguments = arguments.get(platform.system().lower())
if not arguments:
arguments = []
_executables = data["executables"].get(platform.system().lower(), [])
executables = [
ApplicationExecutable(executable)
for executable in _executables
]
self.group = group
self.name = name
self.label = label
self.enabled = enabled
self.use_python_2 = data.get("use_python_2", False)
self.full_name = "/".join((group.name, name))
self.full_label = full_label
self.arguments = arguments
self.executables = executables
self._environment = env
def __repr__(self):
return "<{}> - {}".format(self.__class__.__name__, self.full_name)
@property
def environment(self):
return copy.deepcopy(self._environment)
@property
def manager(self):
return self.group.manager
@property
def host_name(self):
return self.group.host_name
@property
def icon(self):
return self.group.icon
@property
def is_host(self):
return self.group.is_host
def find_executable(self):
"""Try to find existing executable for application.
Returns (str): Path to executable from `executables` or None if any
exists.
"""
for executable in self.executables:
if executable.exists():
return executable
return None
def launch(self, *args, **kwargs):
"""Launch the application.
For this purpose is used manager's launch method to keep logic at one
place.
Arguments must match with manager's launch method. That's why *args
**kwargs are used.
Returns:
subprocess.Popen: Return executed process as Popen object.
"""
return self.manager.launch(self.full_name, *args, **kwargs)
class EnvironmentToolGroup:
"""Hold information about environment tool group.
Environment tool group may hold different variants of same tool and set
environments that are same for all of them.
e.g. "mtoa" may have different versions but all environments except one
are same.
Args:
data (dict): Group information with variants.
manager (ApplicationManager): Manager that creates the group.
"""
def __init__(self, data, manager):
name = data["name"]
label = data["label"]
self.name = name
self.label = label
self._data = data
self.manager = manager
environment = {}
try:
environment = json.loads(data["environment"])
except Exception:
pass
self._environment = environment
variants = data.get("variants") or []
variants_by_name = {}
for variant_data in variants:
tool = EnvironmentTool(variant_data, self)
variants_by_name[tool.name] = tool
self.variants = variants_by_name
def __repr__(self):
return "<{}> - {}".format(self.__class__.__name__, self.name)
def __iter__(self):
for variant in self.variants.values():
yield variant
@property
def environment(self):
return copy.deepcopy(self._environment)
class EnvironmentTool:
"""Hold information about application tool.
Structure of tool information.
Args:
variant_data (dict): Variant data with environments and
host and app variant filters.
group (EnvironmentToolGroup): Name of group which wraps tool.
"""
def __init__(self, variant_data, group):
# Backwards compatibility 3.9.1 - 3.9.2
# - 'variant_data' contained only environments but contain also host
# and application variant filters
name = variant_data["name"]
label = variant_data["label"]
host_names = variant_data["host_names"]
app_variants = variant_data["app_variants"]
environment = {}
try:
environment = json.loads(variant_data["environment"])
except Exception:
pass
self.host_names = host_names
self.app_variants = app_variants
self.name = name
self.variant_label = label
self.label = " ".join((group.label, label))
self.group = group
self._environment = environment
self.full_name = "/".join((group.name, name))
def __repr__(self):
return "<{}> - {}".format(self.__class__.__name__, self.full_name)
@property
def environment(self):
return copy.deepcopy(self._environment)
def is_valid_for_app(self, app):
"""Is tool valid for application.
Args:
app (Application): Application for which are prepared environments.
"""
if self.app_variants and app.full_name not in self.app_variants:
return False
if self.host_names and app.host_name not in self.host_names:
return False
return True

View file

@ -0,0 +1,50 @@
class ApplicationNotFound(Exception):
"""Application was not found in ApplicationManager by name."""
def __init__(self, app_name):
self.app_name = app_name
super(ApplicationNotFound, self).__init__(
"Application \"{}\" was not found.".format(app_name)
)
class ApplicationExecutableNotFound(Exception):
"""Defined executable paths are not available on the machine."""
def __init__(self, application):
self.application = application
details = None
if not application.executables:
msg = (
"Executable paths for application \"{}\"({}) are not set."
)
else:
msg = (
"Defined executable paths for application \"{}\"({})"
" are not available on this machine."
)
details = "Defined paths:"
for executable in application.executables:
details += "\n- " + executable.executable_path
self.msg = msg.format(application.full_label, application.full_name)
self.details = details
exc_mgs = str(self.msg)
if details:
# Is good idea to pass new line symbol to exception message?
exc_mgs += "\n" + details
self.exc_msg = exc_mgs
super(ApplicationExecutableNotFound, self).__init__(exc_mgs)
class ApplicationLaunchFailed(Exception):
"""Application launch failed due to known reason.
Message should be self explanatory as traceback won't be shown.
"""
pass
class MissingRequiredKey(KeyError):
pass

View file

@ -0,0 +1,150 @@
import platform
from abc import ABCMeta, abstractmethod
import six
from ayon_core.lib import Logger
from .defs import LaunchTypes
@six.add_metaclass(ABCMeta)
class LaunchHook:
"""Abstract base class of launch hook."""
# Order of prelaunch hook, will be executed as last if set to None.
order = None
# List of host implementations, skipped if empty.
hosts = set()
# Set of application groups
app_groups = set()
# Set of specific application names
app_names = set()
# Set of platform availability
platforms = set()
# Set of launch types for which is available
# - if empty then is available for all launch types
# - by default has 'local' which is most common reason for launc hooks
launch_types = {LaunchTypes.local}
def __init__(self, launch_context):
"""Constructor of launch hook.
Always should be called
"""
self.log = Logger.get_logger(self.__class__.__name__)
self.launch_context = launch_context
is_valid = self.class_validation(launch_context)
if is_valid:
is_valid = self.validate()
self.is_valid = is_valid
@classmethod
def class_validation(cls, launch_context):
"""Validation of class attributes by launch context.
Args:
launch_context (ApplicationLaunchContext): Context of launching
application.
Returns:
bool: Is launch hook valid for the context by class attributes.
"""
if cls.platforms:
low_platforms = tuple(
_platform.lower()
for _platform in cls.platforms
)
if platform.system().lower() not in low_platforms:
return False
if cls.hosts:
if launch_context.host_name not in cls.hosts:
return False
if cls.app_groups:
if launch_context.app_group.name not in cls.app_groups:
return False
if cls.app_names:
if launch_context.app_name not in cls.app_names:
return False
if cls.launch_types:
if launch_context.launch_type not in cls.launch_types:
return False
return True
@property
def data(self):
return self.launch_context.data
@property
def application(self):
return getattr(self.launch_context, "application", None)
@property
def manager(self):
return getattr(self.application, "manager", None)
@property
def host_name(self):
return getattr(self.application, "host_name", None)
@property
def app_group(self):
return getattr(self.application, "group", None)
@property
def app_name(self):
return getattr(self.application, "full_name", None)
@property
def addons_manager(self):
return getattr(self.launch_context, "addons_manager", None)
@property
def modules_manager(self):
"""
Deprecated:
Use 'addons_wrapper' instead.
"""
return self.addons_manager
def validate(self):
"""Optional validation of launch hook on initialization.
Returns:
bool: Hook is valid (True) or invalid (False).
"""
# QUESTION Not sure if this method has any usable potential.
# - maybe result can be based on settings
return True
@abstractmethod
def execute(self, *args, **kwargs):
"""Abstract execute method where logic of hook is."""
pass
class PreLaunchHook(LaunchHook):
"""Abstract class of prelaunch hook.
This launch hook will be processed before application is launched.
If any exception will happen during processing the application won't be
launched.
"""
class PostLaunchHook(LaunchHook):
"""Abstract class of postlaunch hook.
This launch hook will be processed after application is launched.
Nothing will happen if any exception will happen during processing. And
processing of other postlaunch hooks won't stop either.
"""

View file

@ -0,0 +1,676 @@
import os
import sys
import copy
import json
import tempfile
import platform
import inspect
import subprocess
import six
from ayon_core import AYON_CORE_ROOT
from ayon_core.settings import get_studio_settings
from ayon_core.lib import (
Logger,
modules_from_path,
classes_from_module,
get_linux_launcher_args,
)
from ayon_core.addon import AddonsManager
from .constants import DEFAULT_ENV_SUBGROUP
from .exceptions import (
ApplicationNotFound,
ApplicationExecutableNotFound,
)
from .hooks import PostLaunchHook, PreLaunchHook
from .defs import EnvironmentToolGroup, ApplicationGroup, LaunchTypes
class ApplicationManager:
"""Load applications and tools and store them by their full name.
Args:
studio_settings (dict): Preloaded studio settings. When passed manager
will always use these values. Gives ability to create manager
using different settings.
"""
def __init__(self, studio_settings=None):
self.log = Logger.get_logger(self.__class__.__name__)
self.app_groups = {}
self.applications = {}
self.tool_groups = {}
self.tools = {}
self._studio_settings = studio_settings
self.refresh()
def set_studio_settings(self, studio_settings):
"""Ability to change init system settings.
This will trigger refresh of manager.
"""
self._studio_settings = studio_settings
self.refresh()
def refresh(self):
"""Refresh applications from settings."""
self.app_groups.clear()
self.applications.clear()
self.tool_groups.clear()
self.tools.clear()
if self._studio_settings is not None:
settings = copy.deepcopy(self._studio_settings)
else:
settings = get_studio_settings(
clear_metadata=False, exclude_locals=False
)
applications_addon_settings = settings["applications"]
# Prepare known applications
app_defs = applications_addon_settings["applications"]
additional_apps = app_defs.pop("additional_apps")
for additional_app in additional_apps:
app_name = additional_app.pop("name")
if app_name in app_defs:
self.log.warning((
"Additional application '{}' is already"
" in built-in applications."
).format(app_name))
app_defs[app_name] = additional_app
for group_name, variant_defs in app_defs.items():
group = ApplicationGroup(group_name, variant_defs, self)
self.app_groups[group_name] = group
for app in group:
self.applications[app.full_name] = app
tools_definitions = applications_addon_settings["tool_groups"]
for tool_group_data in tools_definitions:
group = EnvironmentToolGroup(tool_group_data, self)
self.tool_groups[group.name] = group
for tool in group:
self.tools[tool.full_name] = tool
def find_latest_available_variant_for_group(self, group_name):
group = self.app_groups.get(group_name)
if group is None or not group.enabled:
return None
output = None
for _, variant in reversed(sorted(group.variants.items())):
executable = variant.find_executable()
if executable:
output = variant
break
return output
def create_launch_context(self, app_name, **data):
"""Prepare launch context for application.
Args:
app_name (str): Name of application that should be launched.
**data (Any): Any additional data. Data may be used during
Returns:
ApplicationLaunchContext: Launch context for application.
Raises:
ApplicationNotFound: Application was not found by entered name.
"""
app = self.applications.get(app_name)
if not app:
raise ApplicationNotFound(app_name)
executable = app.find_executable()
return ApplicationLaunchContext(
app, executable, **data
)
def launch_with_context(self, launch_context):
"""Launch application using existing launch context.
Args:
launch_context (ApplicationLaunchContext): Prepared launch
context.
"""
if not launch_context.executable:
raise ApplicationExecutableNotFound(launch_context.application)
return launch_context.launch()
def launch(self, app_name, **data):
"""Launch procedure.
For host application it's expected to contain "project_name",
"folder_path" and "task_name".
Args:
app_name (str): Name of application that should be launched.
**data (dict): Any additional data. Data may be used during
preparation to store objects usable in multiple places.
Raises:
ApplicationNotFound: Application was not found by entered
argument `app_name`.
ApplicationExecutableNotFound: Executables in application definition
were not found on this machine.
ApplicationLaunchFailed: Something important for application launch
failed. Exception should contain explanation message,
traceback should not be needed.
"""
context = self.create_launch_context(app_name, **data)
return self.launch_with_context(context)
class ApplicationLaunchContext:
"""Context of launching application.
Main purpose of context is to prepare launch arguments and keyword
arguments for new process. Most important part of keyword arguments
preparations are environment variables.
During the whole process is possible to use `data` attribute to store
object usable in multiple places.
Launch arguments are strings in list. It is possible to "chain" argument
when order of them matters. That is possible to do with adding list where
order is right and should not change.
NOTE: This is recommendation, not requirement.
e.g.: `["nuke.exe", "--NukeX"]` -> In this case any part of process may
insert argument between `nuke.exe` and `--NukeX`. To keep them together
it is better to wrap them in another list: `[["nuke.exe", "--NukeX"]]`.
Notes:
It is possible to use launch context only to prepare environment
variables. In that case `executable` may be None and can be used
'run_prelaunch_hooks' method to run prelaunch hooks which prepare
them.
Args:
application (Application): Application definition.
executable (ApplicationExecutable): Object with path to executable.
env_group (Optional[str]): Environment variable group. If not set
'DEFAULT_ENV_SUBGROUP' is used.
launch_type (Optional[str]): Launch type. If not set 'local' is used.
**data (dict): Any additional data. Data may be used during
preparation to store objects usable in multiple places.
"""
def __init__(
self,
application,
executable,
env_group=None,
launch_type=None,
**data
):
# Application object
self.application = application
self.addons_manager = AddonsManager()
# Logger
logger_name = "{}-{}".format(self.__class__.__name__,
self.application.full_name)
self.log = Logger.get_logger(logger_name)
self.executable = executable
if launch_type is None:
launch_type = LaunchTypes.local
self.launch_type = launch_type
if env_group is None:
env_group = DEFAULT_ENV_SUBGROUP
self.env_group = env_group
self.data = dict(data)
launch_args = []
if executable is not None:
launch_args = executable.as_args()
# subprocess.Popen launch arguments (first argument in constructor)
self.launch_args = launch_args
self.launch_args.extend(application.arguments)
if self.data.get("app_args"):
self.launch_args.extend(self.data.pop("app_args"))
# Handle launch environemtns
src_env = self.data.pop("env", None)
if src_env is not None and not isinstance(src_env, dict):
self.log.warning((
"Passed `env` kwarg has invalid type: {}. Expected: `dict`."
" Using `os.environ` instead."
).format(str(type(src_env))))
src_env = None
if src_env is None:
src_env = os.environ
ignored_env = {"QT_API", }
env = {
key: str(value)
for key, value in src_env.items()
if key not in ignored_env
}
# subprocess.Popen keyword arguments
self.kwargs = {"env": env}
if platform.system().lower() == "windows":
# Detach new process from currently running process on Windows
flags = (
subprocess.CREATE_NEW_PROCESS_GROUP
| subprocess.DETACHED_PROCESS
)
self.kwargs["creationflags"] = flags
if not sys.stdout:
self.kwargs["stdout"] = subprocess.DEVNULL
self.kwargs["stderr"] = subprocess.DEVNULL
self.prelaunch_hooks = None
self.postlaunch_hooks = None
self.process = None
self._prelaunch_hooks_executed = False
@property
def env(self):
if (
"env" not in self.kwargs
or self.kwargs["env"] is None
):
self.kwargs["env"] = {}
return self.kwargs["env"]
@env.setter
def env(self, value):
if not isinstance(value, dict):
raise ValueError(
"'env' attribute expect 'dict' object. Got: {}".format(
str(type(value))
)
)
self.kwargs["env"] = value
@property
def modules_manager(self):
"""
Deprecated:
Use 'addons_manager' instead.
"""
return self.addons_manager
def _collect_addons_launch_hook_paths(self):
"""Helper to collect application launch hooks from addons.
Module have to have implemented 'get_launch_hook_paths' method which
can expect application as argument or nothing.
Returns:
List[str]: Paths to launch hook directories.
"""
expected_types = (list, tuple, set)
output = []
for module in self.addons_manager.get_enabled_addons():
# Skip module if does not have implemented 'get_launch_hook_paths'
func = getattr(module, "get_launch_hook_paths", None)
if func is None:
continue
func = module.get_launch_hook_paths
if hasattr(inspect, "signature"):
sig = inspect.signature(func)
expect_args = len(sig.parameters) > 0
else:
expect_args = len(inspect.getargspec(func)[0]) > 0
# Pass application argument if method expect it.
try:
if expect_args:
hook_paths = func(self.application)
else:
hook_paths = func()
except Exception:
self.log.warning(
"Failed to call 'get_launch_hook_paths'",
exc_info=True
)
continue
if not hook_paths:
continue
# Convert string to list
if isinstance(hook_paths, six.string_types):
hook_paths = [hook_paths]
# Skip invalid types
if not isinstance(hook_paths, expected_types):
self.log.warning((
"Result of `get_launch_hook_paths`"
" has invalid type {}. Expected {}"
).format(type(hook_paths), expected_types))
continue
output.extend(hook_paths)
return output
def paths_to_launch_hooks(self):
"""Directory paths where to look for launch hooks."""
# This method has potential to be part of application manager (maybe).
paths = []
# TODO load additional studio paths from settings
global_hooks_dir = os.path.join(AYON_CORE_ROOT, "hooks")
hooks_dirs = [
global_hooks_dir
]
if self.host_name:
# If host requires launch hooks and is module then launch hooks
# should be collected using 'collect_launch_hook_paths'
# - module have to implement 'get_launch_hook_paths'
host_module = self.addons_manager.get_host_addon(self.host_name)
if not host_module:
hooks_dirs.append(os.path.join(
AYON_CORE_ROOT, "hosts", self.host_name, "hooks"
))
for path in hooks_dirs:
if (
os.path.exists(path)
and os.path.isdir(path)
and path not in paths
):
paths.append(path)
# Load modules paths
paths.extend(self._collect_addons_launch_hook_paths())
return paths
def discover_launch_hooks(self, force=False):
"""Load and prepare launch hooks."""
if (
self.prelaunch_hooks is not None
or self.postlaunch_hooks is not None
):
if not force:
self.log.info("Launch hooks were already discovered.")
return
self.prelaunch_hooks.clear()
self.postlaunch_hooks.clear()
self.log.debug("Discovery of launch hooks started.")
paths = self.paths_to_launch_hooks()
self.log.debug("Paths searched for launch hooks:\n{}".format(
"\n".join("- {}".format(path) for path in paths)
))
all_classes = {
"pre": [],
"post": []
}
for path in paths:
if not os.path.exists(path):
self.log.info(
"Path to launch hooks does not exist: \"{}\"".format(path)
)
continue
modules, _crashed = modules_from_path(path)
for _filepath, module in modules:
all_classes["pre"].extend(
classes_from_module(PreLaunchHook, module)
)
all_classes["post"].extend(
classes_from_module(PostLaunchHook, module)
)
for launch_type, classes in all_classes.items():
hooks_with_order = []
hooks_without_order = []
for klass in classes:
try:
hook = klass(self)
if not hook.is_valid:
self.log.debug(
"Skipped hook invalid for current launch context: "
"{}".format(klass.__name__)
)
continue
if inspect.isabstract(hook):
self.log.debug("Skipped abstract hook: {}".format(
klass.__name__
))
continue
# Separate hooks by pre/post class
if hook.order is None:
hooks_without_order.append(hook)
else:
hooks_with_order.append(hook)
except Exception:
self.log.warning(
"Initialization of hook failed: "
"{}".format(klass.__name__),
exc_info=True
)
# Sort hooks with order by order
ordered_hooks = list(sorted(
hooks_with_order, key=lambda obj: obj.order
))
# Extend ordered hooks with hooks without defined order
ordered_hooks.extend(hooks_without_order)
if launch_type == "pre":
self.prelaunch_hooks = ordered_hooks
else:
self.postlaunch_hooks = ordered_hooks
self.log.debug("Found {} prelaunch and {} postlaunch hooks.".format(
len(self.prelaunch_hooks), len(self.postlaunch_hooks)
))
@property
def app_name(self):
return self.application.name
@property
def host_name(self):
return self.application.host_name
@property
def app_group(self):
return self.application.group
@property
def manager(self):
return self.application.manager
def _run_process(self):
# Windows and MacOS have easier process start
low_platform = platform.system().lower()
if low_platform in ("windows", "darwin"):
return subprocess.Popen(self.launch_args, **self.kwargs)
# Linux uses mid process
# - it is possible that the mid process executable is not
# available for this version of AYON in that case use standard
# launch
launch_args = get_linux_launcher_args()
if launch_args is None:
return subprocess.Popen(self.launch_args, **self.kwargs)
# Prepare data that will be passed to midprocess
# - store arguments to a json and pass path to json as last argument
# - pass environments to set
app_env = self.kwargs.pop("env", {})
json_data = {
"args": self.launch_args,
"env": app_env
}
if app_env:
# Filter environments of subprocess
self.kwargs["env"] = {
key: value
for key, value in os.environ.items()
if key in app_env
}
# Create temp file
json_temp = tempfile.NamedTemporaryFile(
mode="w", prefix="op_app_args", suffix=".json", delete=False
)
json_temp.close()
json_temp_filpath = json_temp.name
with open(json_temp_filpath, "w") as stream:
json.dump(json_data, stream)
launch_args.append(json_temp_filpath)
# Create mid-process which will launch application
process = subprocess.Popen(launch_args, **self.kwargs)
# Wait until the process finishes
# - This is important! The process would stay in "open" state.
process.wait()
# Remove the temp file
os.remove(json_temp_filpath)
# Return process which is already terminated
return process
def run_prelaunch_hooks(self):
"""Run prelaunch hooks.
This method will be executed only once, any future calls will skip
the processing.
"""
if self._prelaunch_hooks_executed:
self.log.warning("Prelaunch hooks were already executed.")
return
# Discover launch hooks
self.discover_launch_hooks()
# Execute prelaunch hooks
for prelaunch_hook in self.prelaunch_hooks:
self.log.debug("Executing prelaunch hook: {}".format(
str(prelaunch_hook.__class__.__name__)
))
prelaunch_hook.execute()
self._prelaunch_hooks_executed = True
def launch(self):
"""Collect data for new process and then create it.
This method must not be executed more than once.
Returns:
subprocess.Popen: Created process as Popen object.
"""
if self.process is not None:
self.log.warning("Application was already launched.")
return
if not self._prelaunch_hooks_executed:
self.run_prelaunch_hooks()
self.log.debug("All prelaunch hook executed. Starting new process.")
# Prepare subprocess args
args_len_str = ""
if isinstance(self.launch_args, str):
args = self.launch_args
else:
args = self.clear_launch_args(self.launch_args)
args_len_str = " ({})".format(len(args))
self.log.info(
"Launching \"{}\" with args{}: {}".format(
self.application.full_name, args_len_str, args
)
)
self.launch_args = args
# Run process
self.process = self._run_process()
# Process post launch hooks
for postlaunch_hook in self.postlaunch_hooks:
self.log.debug("Executing postlaunch hook: {}".format(
str(postlaunch_hook.__class__.__name__)
))
# TODO how to handle errors?
# - store to variable to let them accessible?
try:
postlaunch_hook.execute()
except Exception:
self.log.warning(
"After launch procedures were not successful.",
exc_info=True
)
self.log.debug("Launch of {} finished.".format(
self.application.full_name
))
return self.process
@staticmethod
def clear_launch_args(args):
"""Collect launch arguments to final order.
Launch argument should be list that may contain another lists this
function will upack inner lists and keep ordering.
```
# source
[ [ arg1, [ arg2, arg3 ] ], arg4, [arg5, arg6]]
# result
[ arg1, arg2, arg3, arg4, arg5, arg6]
Args:
args (list): Source arguments in list may contain inner lists.
Return:
list: Unpacked arguments.
"""
if isinstance(args, str):
return args
all_cleared = False
while not all_cleared:
all_cleared = True
new_args = []
for arg in args:
if isinstance(arg, (list, tuple, set)):
all_cleared = False
for _arg in arg:
new_args.append(_arg)
else:
new_args.append(arg)
args = new_args
return args

View file

@ -0,0 +1,48 @@
"""
Run after global plugin 'CollectHostName' in ayon_core.
Requires:
None
Provides:
context -> hostName (str)
context -> appName (str)
context -> appLabel (str)
"""
import os
import pyblish.api
from ayon_applications import ApplicationManager
class CollectAppName(pyblish.api.ContextPlugin):
"""Collect avalon host name to context."""
label = "Collect App Name"
order = pyblish.api.CollectorOrder - 0.499999
def process(self, context):
host_name = context.data.get("hostName")
app_name = context.data.get("appName")
app_label = context.data.get("appLabel")
# Don't override value if is already set
if host_name and app_name and app_label:
return
# Use AYON_APP_NAME to get full app name
if not app_name:
app_name = os.environ.get("AYON_APP_NAME")
# Fill missing values based on app full name
if (not host_name or not app_label) and app_name:
app_manager = ApplicationManager()
app = app_manager.applications.get(app_name)
if app:
if not host_name:
host_name = app.host_name
if not app_label:
app_label = app.full_label
context.data["hostName"] = host_name
context.data["appName"] = app_name
context.data["appLabel"] = app_label

View file

@ -0,0 +1,609 @@
import os
import copy
import json
import platform
import collections
import six
import acre
from ayon_core import AYON_CORE_ROOT
from ayon_core.settings import get_project_settings
from ayon_core.lib import Logger, get_ayon_username
from ayon_core.addon import AddonsManager
from ayon_core.pipeline import HOST_WORKFILE_EXTENSIONS
from ayon_core.pipeline.template_data import get_template_data
from ayon_core.pipeline.workfile import (
get_workfile_template_key,
get_workdir_with_workdir_data,
get_last_workfile,
should_use_last_workfile_on_launch,
should_open_workfiles_tool_on_launch,
)
from .constants import PLATFORM_NAMES, DEFAULT_ENV_SUBGROUP
from .exceptions import MissingRequiredKey, ApplicationLaunchFailed
from .manager import ApplicationManager
def parse_environments(env_data, env_group=None, platform_name=None):
"""Parse environment values from settings byt group and platform.
Data may contain up to 2 hierarchical levels of dictionaries. At the end
of the last level must be string or list. List is joined using platform
specific joiner (';' for windows and ':' for linux and mac).
Hierarchical levels can contain keys for subgroups and platform name.
Platform specific values must be always last level of dictionary. Platform
names are "windows" (MS Windows), "linux" (any linux distribution) and
"darwin" (any MacOS distribution).
Subgroups are helpers added mainly for standard and on farm usage. Farm
may require different environments for e.g. licence related values or
plugins. Default subgroup is "standard".
Examples:
```
{
# Unchanged value
"ENV_KEY1": "value",
# Empty values are kept (unset environment variable)
"ENV_KEY2": "",
# Join list values with ':' or ';'
"ENV_KEY3": ["value1", "value2"],
# Environment groups
"ENV_KEY4": {
"standard": "DEMO_SERVER_URL",
"farm": "LICENCE_SERVER_URL"
},
# Platform specific (and only for windows and mac)
"ENV_KEY5": {
"windows": "windows value",
"darwin": ["value 1", "value 2"]
},
# Environment groups and platform combination
"ENV_KEY6": {
"farm": "FARM_VALUE",
"standard": {
"windows": ["value1", "value2"],
"linux": "value1",
"darwin": ""
}
}
}
```
"""
output = {}
if not env_data:
return output
if not env_group:
env_group = DEFAULT_ENV_SUBGROUP
if not platform_name:
platform_name = platform.system().lower()
for key, value in env_data.items():
if isinstance(value, dict):
# Look if any key is platform key
# - expect that represents environment group if does not contain
# platform keys
if not PLATFORM_NAMES.intersection(set(value.keys())):
# Skip the key if group is not available
if env_group not in value:
continue
value = value[env_group]
# Check again if value is dictionary
# - this time there should be only platform keys
if isinstance(value, dict):
value = value.get(platform_name)
# Check if value is list and join it's values
# QUESTION Should empty values be skipped?
if isinstance(value, (list, tuple)):
value = os.pathsep.join(value)
# Set key to output if value is string
if isinstance(value, six.string_types):
output[key] = value
return output
class EnvironmentPrepData(dict):
"""Helper dictionary for storin temp data during environment prep.
Args:
data (dict): Data must contain required keys.
"""
required_keys = (
"project_entity", "folder_entity", "task_entity", "app", "anatomy"
)
def __init__(self, data):
for key in self.required_keys:
if key not in data:
raise MissingRequiredKey(key)
if not data.get("log"):
data["log"] = Logger.get_logger("EnvironmentPrepData")
if data.get("env") is None:
data["env"] = os.environ.copy()
project_name = data["project_entity"]["name"]
if "project_settings" not in data:
data["project_settings"] = get_project_settings(project_name)
super(EnvironmentPrepData, self).__init__(data)
def get_app_environments_for_context(
project_name,
folder_path,
task_name,
app_name,
env_group=None,
launch_type=None,
env=None,
addons_manager=None
):
"""Prepare environment variables by context.
Args:
project_name (str): Name of project.
folder_path (str): Folder path.
task_name (str): Name of task.
app_name (str): Name of application that is launched and can be found
by ApplicationManager.
env_group (Optional[str]): Name of environment group. If not passed
default group is used.
launch_type (Optional[str]): Type for which prelaunch hooks are
executed.
env (Optional[dict[str, str]]): Initial environment variables.
`os.environ` is used when not passed.
addons_manager (Optional[AddonsManager]): Initialized modules
manager.
Returns:
dict: Environments for passed context and application.
"""
# Prepare app object which can be obtained only from ApplicationManager
app_manager = ApplicationManager()
context = app_manager.create_launch_context(
app_name,
project_name=project_name,
folder_path=folder_path,
task_name=task_name,
env_group=env_group,
launch_type=launch_type,
env=env,
addons_manager=addons_manager,
modules_manager=addons_manager,
)
context.run_prelaunch_hooks()
return context.env
def _merge_env(env, current_env):
"""Modified function(merge) from acre module."""
result = current_env.copy()
for key, value in env.items():
# Keep missing keys by not filling `missing` kwarg
value = acre.lib.partial_format(value, data=current_env)
result[key] = value
return result
def _add_python_version_paths(app, env, logger, addons_manager):
"""Add vendor packages specific for a Python version."""
for addon in addons_manager.get_enabled_addons():
addon.modify_application_launch_arguments(app, env)
# Skip adding if host name is not set
if not app.host_name:
return
# Add Python 2/3 modules
python_vendor_dir = os.path.join(
AYON_CORE_ROOT,
"vendor",
"python"
)
if app.use_python_2:
pythonpath = os.path.join(python_vendor_dir, "python_2")
else:
pythonpath = os.path.join(python_vendor_dir, "python_3")
if not os.path.exists(pythonpath):
return
logger.debug("Adding Python version specific paths to PYTHONPATH")
python_paths = [pythonpath]
# Load PYTHONPATH from current launch context
python_path = env.get("PYTHONPATH")
if python_path:
python_paths.append(python_path)
# Set new PYTHONPATH to launch context environments
env["PYTHONPATH"] = os.pathsep.join(python_paths)
def prepare_app_environments(
data, env_group=None, implementation_envs=True, addons_manager=None
):
"""Modify launch environments based on launched app and context.
Args:
data (EnvironmentPrepData): Dictionary where result and intermediate
result will be stored.
"""
app = data["app"]
log = data["log"]
source_env = data["env"].copy()
if addons_manager is None:
addons_manager = AddonsManager()
_add_python_version_paths(app, source_env, log, addons_manager)
# Use environments from local settings
filtered_local_envs = {}
# NOTE Overrides for environment variables are not implemented in AYON.
# project_settings = data["project_settings"]
# whitelist_envs = project_settings["general"].get("local_env_white_list")
# if whitelist_envs:
# local_settings = get_local_settings()
# local_envs = local_settings.get("environments") or {}
# filtered_local_envs = {
# key: value
# for key, value in local_envs.items()
# if key in whitelist_envs
# }
# Apply local environment variables for already existing values
for key, value in filtered_local_envs.items():
if key in source_env:
source_env[key] = value
# `app_and_tool_labels` has debug purpose
app_and_tool_labels = [app.full_name]
# Environments for application
environments = [
app.group.environment,
app.environment
]
folder_entity = data.get("folder_entity")
# Add tools environments
groups_by_name = {}
tool_by_group_name = collections.defaultdict(dict)
if folder_entity:
# Make sure each tool group can be added only once
for key in folder_entity["attrib"].get("tools") or []:
tool = app.manager.tools.get(key)
if not tool or not tool.is_valid_for_app(app):
continue
groups_by_name[tool.group.name] = tool.group
tool_by_group_name[tool.group.name][tool.name] = tool
for group_name in sorted(groups_by_name.keys()):
group = groups_by_name[group_name]
environments.append(group.environment)
for tool_name in sorted(tool_by_group_name[group_name].keys()):
tool = tool_by_group_name[group_name][tool_name]
environments.append(tool.environment)
app_and_tool_labels.append(tool.full_name)
log.debug(
"Will add environments for apps and tools: {}".format(
", ".join(app_and_tool_labels)
)
)
env_values = {}
for _env_values in environments:
if not _env_values:
continue
# Choose right platform
tool_env = parse_environments(_env_values, env_group)
# Apply local environment variables
# - must happen between all values because they may be used during
# merge
for key, value in filtered_local_envs.items():
if key in tool_env:
tool_env[key] = value
# Merge dictionaries
env_values = _merge_env(tool_env, env_values)
merged_env = _merge_env(env_values, source_env)
loaded_env = acre.compute(merged_env, cleanup=False)
final_env = None
# Add host specific environments
if app.host_name and implementation_envs:
host_addon = addons_manager.get_host_addon(app.host_name)
add_implementation_envs = None
if host_addon:
add_implementation_envs = getattr(
host_addon, "add_implementation_envs", None
)
if add_implementation_envs:
# Function may only modify passed dict without returning value
final_env = add_implementation_envs(loaded_env, app)
if final_env is None:
final_env = loaded_env
keys_to_remove = set(source_env.keys()) - set(final_env.keys())
# Update env
data["env"].update(final_env)
for key in keys_to_remove:
data["env"].pop(key, None)
def apply_project_environments_value(
project_name, env, project_settings=None, env_group=None
):
"""Apply project specific environments on passed environments.
The environments are applied on passed `env` argument value so it is not
required to apply changes back.
Args:
project_name (str): Name of project for which environments should be
received.
env (dict): Environment values on which project specific environments
will be applied.
project_settings (dict): Project settings for passed project name.
Optional if project settings are already prepared.
Returns:
dict: Passed env values with applied project environments.
Raises:
KeyError: If project settings do not contain keys for project specific
environments.
"""
if project_settings is None:
project_settings = get_project_settings(project_name)
env_value = project_settings["core"]["project_environments"]
if env_value:
env_value = json.loads(env_value)
parsed_value = parse_environments(env_value, env_group)
env.update(acre.compute(
_merge_env(parsed_value, env),
cleanup=False
))
return env
def prepare_context_environments(data, env_group=None, addons_manager=None):
"""Modify launch environments with context data for launched host.
Args:
data (EnvironmentPrepData): Dictionary where result and intermediate
result will be stored.
"""
# Context environments
log = data["log"]
project_entity = data["project_entity"]
folder_entity = data["folder_entity"]
task_entity = data["task_entity"]
if not project_entity:
log.info(
"Skipping context environments preparation."
" Launch context does not contain required data."
)
return
# Load project specific environments
project_name = project_entity["name"]
project_settings = get_project_settings(project_name)
data["project_settings"] = project_settings
app = data["app"]
context_env = {
"AYON_PROJECT_NAME": project_entity["name"],
"AYON_APP_NAME": app.full_name
}
if folder_entity:
folder_path = folder_entity["path"]
context_env["AYON_FOLDER_PATH"] = folder_path
if task_entity:
context_env["AYON_TASK_NAME"] = task_entity["name"]
log.debug(
"Context environments set:\n{}".format(
json.dumps(context_env, indent=4)
)
)
data["env"].update(context_env)
# Apply project specific environments on current env value
# - apply them once the context environments are set
apply_project_environments_value(
project_name, data["env"], project_settings, env_group
)
if not app.is_host:
return
data["env"]["AYON_HOST_NAME"] = app.host_name
if not folder_entity or not task_entity:
# QUESTION replace with log.info and skip workfile discovery?
# - technically it should be possible to launch host without context
raise ApplicationLaunchFailed(
"Host launch require folder and task context."
)
workdir_data = get_template_data(
project_entity,
folder_entity,
task_entity,
app.host_name,
project_settings
)
data["workdir_data"] = workdir_data
anatomy = data["anatomy"]
task_type = workdir_data["task"]["type"]
# Temp solution how to pass task type to `_prepare_last_workfile`
data["task_type"] = task_type
try:
workdir = get_workdir_with_workdir_data(
workdir_data,
anatomy.project_name,
anatomy,
project_settings=project_settings
)
except Exception as exc:
raise ApplicationLaunchFailed(
"Error in anatomy.format: {}".format(str(exc))
)
if not os.path.exists(workdir):
log.debug(
"Creating workdir folder: \"{}\"".format(workdir)
)
try:
os.makedirs(workdir)
except Exception as exc:
raise ApplicationLaunchFailed(
"Couldn't create workdir because: {}".format(str(exc))
)
data["env"]["AYON_WORKDIR"] = workdir
_prepare_last_workfile(data, workdir, addons_manager)
def _prepare_last_workfile(data, workdir, addons_manager):
"""last workfile workflow preparation.
Function check if should care about last workfile workflow and tries
to find the last workfile. Both information are stored to `data` and
environments.
Last workfile is filled always (with version 1) even if any workfile
exists yet.
Args:
data (EnvironmentPrepData): Dictionary where result and intermediate
result will be stored.
workdir (str): Path to folder where workfiles should be stored.
"""
if not addons_manager:
addons_manager = AddonsManager()
log = data["log"]
_workdir_data = data.get("workdir_data")
if not _workdir_data:
log.info(
"Skipping last workfile preparation."
" Key `workdir_data` not filled."
)
return
app = data["app"]
workdir_data = copy.deepcopy(_workdir_data)
project_name = data["project_name"]
task_name = data["task_name"]
task_type = data["task_type"]
start_last_workfile = data.get("start_last_workfile")
if start_last_workfile is None:
start_last_workfile = should_use_last_workfile_on_launch(
project_name, app.host_name, task_name, task_type
)
else:
log.info("Opening of last workfile was disabled by user")
data["start_last_workfile"] = start_last_workfile
workfile_startup = should_open_workfiles_tool_on_launch(
project_name, app.host_name, task_name, task_type
)
data["workfile_startup"] = workfile_startup
# Store boolean as "0"(False) or "1"(True)
data["env"]["AVALON_OPEN_LAST_WORKFILE"] = (
str(int(bool(start_last_workfile)))
)
data["env"]["AYON_WORKFILE_TOOL_ON_START"] = (
str(int(bool(workfile_startup)))
)
_sub_msg = "" if start_last_workfile else " not"
log.debug(
"Last workfile should{} be opened on start.".format(_sub_msg)
)
# Last workfile path
last_workfile_path = data.get("last_workfile_path") or ""
if not last_workfile_path:
host_addon = addons_manager.get_host_addon(app.host_name)
if host_addon:
extensions = host_addon.get_workfile_extensions()
else:
extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name)
if extensions:
anatomy = data["anatomy"]
project_settings = data["project_settings"]
task_type = workdir_data["task"]["type"]
template_key = get_workfile_template_key(
project_name,
task_type,
app.host_name,
project_settings=project_settings
)
# Find last workfile
file_template = anatomy.get_template_item(
"work", template_key, "file"
).template
workdir_data.update({
"version": 1,
"user": get_ayon_username(),
"ext": extensions[0]
})
last_workfile_path = get_last_workfile(
workdir, file_template, workdir_data, extensions, True
)
if os.path.exists(last_workfile_path):
log.debug((
"Workfiles for launch context does not exists"
" yet but path will be set."
))
log.debug(
"Setting last workfile path: {}".format(last_workfile_path)
)
data["env"]["AYON_LAST_WORKFILE"] = last_workfile_path
data["last_workfile_path"] = last_workfile_path

View file

@ -4,6 +4,7 @@ import os
import sys
import code
import traceback
from pathlib import Path
import click
import acre
@ -11,7 +12,7 @@ import acre
from ayon_core import AYON_CORE_ROOT
from ayon_core.addon import AddonsManager
from ayon_core.settings import get_general_environments
from ayon_core.lib import initialize_ayon_connection
from ayon_core.lib import initialize_ayon_connection, is_running_from_build
from .cli_commands import Commands
@ -81,7 +82,7 @@ main_cli.set_alias("addon", "module")
@main_cli.command()
@click.argument("output_json_path")
@click.option("--project", help="Project name", default=None)
@click.option("--asset", help="Asset name", default=None)
@click.option("--asset", help="Folder path", default=None)
@click.option("--task", help="Task name", default=None)
@click.option("--app", help="Application name", default=None)
@click.option(
@ -96,6 +97,10 @@ def extractenvironments(output_json_path, project, asset, task, app, envgroup):
environments will be extracted.
Context options are "project", "asset", "task", "app"
Deprecated:
This function is deprecated and will be removed in future. Please use
'addon applications extractenvironments ...' instead.
"""
Commands.extractenvironments(
output_json_path, project, asset, task, app, envgroup
@ -127,7 +132,7 @@ def publish_report_viewer():
@main_cli.command()
@click.argument("output_path")
@click.option("--project", help="Define project context")
@click.option("--asset", help="Define asset in project (project must be set)")
@click.option("--folder", help="Define folder in project (project must be set)")
@click.option(
"--strict",
is_flag=True,
@ -136,18 +141,18 @@ def publish_report_viewer():
def contextselection(
output_path,
project,
asset,
folder,
strict
):
"""Show Qt dialog to select context.
Context is project name, asset name and task name. The result is stored
Context is project name, folder path and task name. The result is stored
into json file which path is passed in first argument.
"""
Commands.contextselection(
output_path,
project,
asset,
folder,
strict
)
@ -163,16 +168,27 @@ def run(script):
if not script:
print("Error: missing path to script file.")
return
# Remove first argument if it is the same as AYON executable
# - Forward compatibility with future AYON versions.
# - Current AYON launcher keeps the arguments with first argument but
# future versions might remove it.
first_arg = sys.argv[0]
if is_running_from_build():
comp_path = os.path.join(os.environ["AYON_ROOT"], "start.py")
else:
comp_path = os.getenv("AYON_EXECUTABLE")
# Compare paths and remove first argument if it is the same as AYON
if Path(first_arg).resolve() == Path(comp_path).resolve():
sys.argv.pop(0)
args = sys.argv
args.remove("run")
args.remove(script)
sys.argv = args
# Remove 'run' command from sys.argv
sys.argv.remove("run")
args_string = " ".join(args[1:])
print(f"... running: {script} {args_string}")
runpy.run_path(script, run_name="__main__", )
args_string = " ".join(sys.argv[1:])
print(f"... running: {script} {args_string}")
runpy.run_path(script, run_name="__main__")
@main_cli.command()

View file

@ -2,7 +2,6 @@
"""Implementation of AYON commands."""
import os
import sys
import json
import warnings
@ -58,10 +57,7 @@ class Commands:
"""
from ayon_core.lib import Logger
from ayon_core.lib.applications import (
get_app_environments_for_context,
LaunchTypes,
)
from ayon_core.addon import AddonsManager
from ayon_core.pipeline import (
install_ayon_plugins,
@ -69,7 +65,6 @@ class Commands:
)
# Register target and host
import pyblish.api
import pyblish.util
if not isinstance(path, str):
@ -100,15 +95,13 @@ class Commands:
for plugin_path in publish_paths:
pyblish.api.register_plugin_path(plugin_path)
app_full_name = os.getenv("AYON_APP_NAME")
if app_full_name:
applications_addon = manager.get_enabled_addon("applications")
if applications_addon is not None:
context = get_global_context()
env = get_app_environments_for_context(
env = applications_addon.get_farm_publish_environment_variables(
context["project_name"],
context["folder_path"],
context["task_name"],
app_full_name,
launch_type=LaunchTypes.farm_publish,
)
os.environ.update(env)
@ -150,36 +143,36 @@ class Commands:
log.info("Publish finished.")
@staticmethod
def extractenvironments(output_json_path, project, asset, task, app,
env_group):
def extractenvironments(
output_json_path, project, asset, task, app, env_group
):
"""Produces json file with environment based on project and app.
Called by Deadline plugin to propagate environment into render jobs.
"""
from ayon_core.lib.applications import (
get_app_environments_for_context,
LaunchTypes,
from ayon_core.addon import AddonsManager
warnings.warn(
(
"Command 'extractenvironments' is deprecated and will be"
" removed in future. Please use "
"'addon applications extractenvironments ...' instead."
),
DeprecationWarning
)
if all((project, asset, task, app)):
env = get_app_environments_for_context(
project,
asset,
task,
app,
env_group=env_group,
launch_type=LaunchTypes.farm_render
addons_manager = AddonsManager()
applications_addon = addons_manager.get_enabled_addon("applications")
if applications_addon is None:
raise RuntimeError(
"Applications addon is not available or enabled."
)
else:
env = os.environ.copy()
output_dir = os.path.dirname(output_json_path)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
with open(output_json_path, "w") as file_stream:
json.dump(env, file_stream, indent=4)
# Please ignore the fact this is using private method
applications_addon._cli_extract_environments(
output_json_path, project, asset, task, app, env_group
)
@staticmethod
def contextselection(output_path, project_name, folder_path, strict):

View file

@ -1,6 +1,6 @@
import os
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class AddLastWorkfileToLaunchArgs(PreLaunchHook):

View file

@ -1,7 +1,7 @@
import os
import shutil
from ayon_core.settings import get_project_settings
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.pipeline.workfile import (
get_custom_workfile_template,
get_custom_workfile_template_by_string_context

View file

@ -1,5 +1,5 @@
import os
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.pipeline.workfile import create_workdir_extra_folders

View file

@ -1,7 +1,7 @@
from ayon_api import get_project, get_folder_by_path, get_task_by_name
from ayon_core.lib.applications import (
PreLaunchHook,
from ayon_applications import PreLaunchHook
from ayon_applications.utils import (
EnvironmentPrepData,
prepare_app_environments,
prepare_context_environments

View file

@ -1,5 +1,5 @@
import os
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class LaunchWithTerminal(PreLaunchHook):

View file

@ -1,5 +1,5 @@
import subprocess
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class LaunchNewConsoleApps(PreLaunchHook):

View file

@ -1,4 +1,4 @@
from ayon_core.lib.applications import PreLaunchHook
from ayon_applications import PreLaunchHook
from ayon_core.pipeline.colorspace import get_imageio_config
from ayon_core.pipeline.template_data import get_template_data_with_names

View file

@ -36,23 +36,23 @@ class HostDirmap(object):
host_name,
project_name,
project_settings=None,
sync_module=None
sitesync_addon=None
):
self.host_name = host_name
self.project_name = project_name
self._project_settings = project_settings
self._sync_module = sync_module
self._sitesync_addon = sitesync_addon
# to limit reinit of Modules
self._sync_module_discovered = sync_module is not None
self._sitesync_addon_discovered = sitesync_addon is not None
self._log = None
@property
def sync_module(self):
if not self._sync_module_discovered:
self._sync_module_discovered = True
def sitesync_addon(self):
if not self._sitesync_addon_discovered:
self._sitesync_addon_discovered = True
manager = AddonsManager()
self._sync_module = manager.get("sync_server")
return self._sync_module
self._sitesync_addon = manager.get("sitesync")
return self._sitesync_addon
@property
def project_settings(self):
@ -158,25 +158,25 @@ class HostDirmap(object):
"""
project_name = self.project_name
sync_module = self.sync_module
sitesync_addon = self.sitesync_addon
mapping = {}
if (
sync_module is None
or not sync_module.enabled
or project_name not in sync_module.get_enabled_projects()
sitesync_addon is None
or not sitesync_addon.enabled
or project_name not in sitesync_addon.get_enabled_projects()
):
return mapping
active_site = sync_module.get_local_normalized_site(
sync_module.get_active_site(project_name))
remote_site = sync_module.get_local_normalized_site(
sync_module.get_remote_site(project_name))
active_site = sitesync_addon.get_local_normalized_site(
sitesync_addon.get_active_site(project_name))
remote_site = sitesync_addon.get_local_normalized_site(
sitesync_addon.get_remote_site(project_name))
self.log.debug(
"active {} - remote {}".format(active_site, remote_site)
)
if active_site == "local" and active_site != remote_site:
sync_settings = sync_module.get_sync_project_setting(
sync_settings = sitesync_addon.get_sync_project_setting(
project_name,
exclude_locals=False,
cached=False)
@ -194,7 +194,7 @@ class HostDirmap(object):
self.log.debug("remote overrides {}".format(remote_overrides))
current_platform = platform.system().lower()
remote_provider = sync_module.get_provider_for_site(
remote_provider = sitesync_addon.get_provider_for_site(
project_name, remote_site
)
# dirmap has sense only with regular disk provider, in the workfile

View file

@ -31,6 +31,7 @@ __all__ = [
"get_stub",
# pipeline
"AfterEffectsHost",
"ls",
"containerise",

View file

@ -6,10 +6,7 @@ from ayon_core.lib import (
get_ayon_launcher_args,
is_using_ayon_console,
)
from ayon_core.lib.applications import (
PreLaunchHook,
LaunchTypes,
)
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.aftereffects import get_launch_script_path

View file

@ -1,14 +1,11 @@
import os
import re
import tempfile
import attr
import attr
import pyblish.api
from ayon_core.settings import get_project_settings
from ayon_core.pipeline import publish
from ayon_core.pipeline.publish import RenderInstance
from ayon_core.hosts.aftereffects.api import get_stub

View file

@ -191,7 +191,7 @@ def _process_app_events() -> Optional[float]:
class LaunchQtApp(bpy.types.Operator):
"""A Base class for opertors to launch a Qt app."""
"""A Base class for operators to launch a Qt app."""
_app: QtWidgets.QApplication
_window = Union[QtWidgets.QDialog, ModuleType]

View file

@ -1,6 +1,6 @@
from pathlib import Path
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class AddPythonScriptToLaunchArgs(PreLaunchHook):

View file

@ -2,7 +2,7 @@ import os
import re
import subprocess
from platform import system
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class InstallPySideToBlender(PreLaunchHook):

View file

@ -1,5 +1,5 @@
import subprocess
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class BlenderConsoleWindows(PreLaunchHook):

View file

@ -227,7 +227,7 @@ class BlendLoader(plugin.AssetLoader):
obj.animation_data_create()
obj.animation_data.action = actions[obj.name]
# Restore the old data, but reset memebers, as they don't exist anymore
# Restore the old data, but reset members, as they don't exist anymore
# This avoids a crash, because the memory addresses of those members
# are not valid anymore
old_data["members"] = []

View file

@ -4,7 +4,6 @@ import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin):

View file

@ -4,7 +4,6 @@ import bpy
from ayon_core.pipeline import publish
from ayon_core.hosts.blender.api import plugin
from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY
class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin):

View file

@ -32,7 +32,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin,
tree = bpy.context.scene.node_tree
output_type = "CompositorNodeOutputFile"
output_node = None
# Remove all output nodes that inlcude "AYON" in the name.
# Remove all output nodes that include "AYON" in the name.
# There should be only one.
for node in tree.nodes:
if node.bl_idname == output_type and "AYON" in node.name:

View file

@ -12,7 +12,7 @@ from ayon_core.pipeline.publish import (
import ayon_core.hosts.blender.api.action
class ValidateMeshNoNegativeScale(pyblish.api.Validator,
class ValidateMeshNoNegativeScale(pyblish.api.InstancePlugin,
OptionalPyblishPluginMixin):
"""Ensure that meshes don't have a negative scale."""

View file

@ -3,7 +3,7 @@ import shutil
import winreg
import subprocess
from ayon_core.lib import get_ayon_launcher_args
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.celaction import CELACTION_ROOT_DIR
@ -118,7 +118,7 @@ class CelactionPrelaunchHook(PreLaunchHook):
def workfile_path(self):
workfile_path = self.data["last_workfile_path"]
# copy workfile from template if doesnt exist any on path
# copy workfile from template if doesn't exist any on path
if not os.path.exists(workfile_path):
# TODO add ability to set different template workfile path via
# settings

View file

@ -3,11 +3,11 @@ import sys
from pprint import pformat
class CollectCelactionCliKwargs(pyblish.api.Collector):
class CollectCelactionCliKwargs(pyblish.api.ContextPlugin):
""" Collects all keyword arguments passed from the terminal """
label = "Collect Celaction Cli Kwargs"
order = pyblish.api.Collector.order - 0.1
order = pyblish.api.CollectorOrder - 0.1
def process(self, context):
args = list(sys.argv[1:])

View file

@ -38,7 +38,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin):
render_path = r_template_item["path"].format_strict(anatomy_data)
self.log.debug("__ render_path: `{}`".format(render_path))
# create dir if it doesnt exists
# create dir if it doesn't exists
try:
if not os.path.isdir(render_dir):
os.makedirs(render_dir, exist_ok=True)

View file

@ -23,7 +23,7 @@ from .lib import (
reset_segment_selection,
get_segment_attributes,
get_clips_in_reels,
get_reformated_filename,
get_reformatted_filename,
get_frame_from_filename,
get_padding_from_filename,
maintained_object_duplication,
@ -101,7 +101,7 @@ __all__ = [
"reset_segment_selection",
"get_segment_attributes",
"get_clips_in_reels",
"get_reformated_filename",
"get_reformatted_filename",
"get_frame_from_filename",
"get_padding_from_filename",
"maintained_object_duplication",

View file

@ -607,7 +607,7 @@ def get_clips_in_reels(project):
return output_clips
def get_reformated_filename(filename, padded=True):
def get_reformatted_filename(filename, padded=True):
"""
Return fixed python expression path
@ -615,10 +615,10 @@ def get_reformated_filename(filename, padded=True):
filename (str): file name
Returns:
type: string with reformated path
type: string with reformatted path
Example:
get_reformated_filename("plate.1001.exr") > plate.%04d.exr
get_reformatted_filename("plate.1001.exr") > plate.%04d.exr
"""
found = FRAME_PATTERN.search(filename)
@ -980,7 +980,7 @@ class MediaInfoFile(object):
@property
def file_pattern(self):
"""Clips file patter
"""Clips file pattern.
Returns:
str: file pattern. ex. file.[1-2].exr

View file

@ -644,13 +644,13 @@ class PublishableClip:
"families": [self.base_product_type, self.product_type]
}
def _convert_to_entity(self, type, template):
def _convert_to_entity(self, src_type, template):
""" Converting input key to key with type. """
# convert to entity type
entity_type = self.types.get(type, None)
folder_type = self.types.get(src_type, None)
assert entity_type, "Missing entity type for `{}`".format(
type
assert folder_type, "Missing folder type for `{}`".format(
src_type
)
# first collect formatting data to use for formatting template
@ -661,7 +661,7 @@ class PublishableClip:
formatting_data[_k] = value
return {
"entity_type": entity_type,
"folder_type": folder_type,
"entity_name": template.format(
**formatting_data
)
@ -1018,7 +1018,7 @@ class OpenClipSolver(flib.MediaInfoFile):
self.feed_version_name))
else:
self.log.debug("adding new track element ..")
# create new track as it doesnt exists yet
# create new track as it doesn't exist yet
# set current version to feeds on tmp
tmp_xml_feeds = tmp_xml_track.find('feeds')
tmp_xml_feeds.set('currentVersion', self.feed_version_name)

View file

@ -9,7 +9,7 @@ from ayon_core.lib import (
get_ayon_username,
run_subprocess,
)
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts import flame as opflame

View file

@ -256,7 +256,7 @@ def create_otio_reference(clip_data, fps=None):
if not otio_ex_ref_item:
dirname, file_name = os.path.split(path)
file_name = utils.get_reformated_filename(file_name, padded=False)
file_name = utils.get_reformatted_filename(file_name, padded=False)
reformated_path = os.path.join(dirname, file_name)
# in case old OTIO or video file create `ExternalReference`
otio_ex_ref_item = otio.schema.ExternalReference(

View file

@ -21,7 +21,7 @@ def frames_to_seconds(frames, framerate):
return otio.opentime.to_seconds(rt)
def get_reformated_filename(filename, padded=True):
def get_reformatted_filename(filename, padded=True):
"""
Return fixed python expression path
@ -29,10 +29,10 @@ def get_reformated_filename(filename, padded=True):
filename (str): file name
Returns:
type: string with reformated path
type: string with reformatted path
Example:
get_reformated_filename("plate.1001.exr") > plate.%04d.exr
get_reformatted_filename("plate.1001.exr") > plate.%04d.exr
"""
found = FRAME_PATTERN.search(filename)

View file

@ -17,7 +17,7 @@ class CreateShotClip(opfapi.Creator):
presets = deepcopy(self.presets)
gui_inputs = self.get_gui_inputs()
# get key pares from presets and match it on ui inputs
# get key pairs from presets and match it on ui inputs
for k, v in gui_inputs.items():
if v["type"] in ("dict", "section"):
# nested dictionary (only one level allowed
@ -236,7 +236,7 @@ class CreateShotClip(opfapi.Creator):
"type": "QCheckBox",
"label": "Source resolution",
"target": "tag",
"toolTip": "Is resloution taken from timeline or source?", # noqa
"toolTip": "Is resolution taken from timeline or source?", # noqa
"order": 4},
}
},

View file

@ -37,7 +37,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
self.otio_timeline = context.data["otioTimeline"]
self.fps = context.data["fps"]
# process all sellected
# process all selected
for segment in selected_segments:
# get openpype tag data
marker_data = opfapi.get_segment_data_marker(segment)
@ -100,6 +100,12 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
marker_data["handleEnd"] = min(
marker_data["handleEnd"], tail)
# Backward compatibility fix of 'entity_type' > 'folder_type'
if "parents" in marker_data:
for parent in marker_data["parents"]:
if "entity_type" in parent:
parent["folder_type"] = parent.pop("entity_type")
workfile_start = self._set_workfile_start(marker_data)
with_audio = bool(marker_data.pop("audio"))

View file

@ -396,7 +396,7 @@ class FtrackEntityOperator:
entity = session.query(query).first()
# if entity doesnt exist then create one
# if entity doesn't exist then create one
if not entity:
entity = self.create_ftrack_entity(
session,

View file

@ -5,7 +5,9 @@ import contextlib
from ayon_core.lib import Logger
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline import registered_host
from ayon_core.pipeline.create import CreateContext
from ayon_core.pipeline.context_tools import get_current_folder_entity
self = sys.modules[__name__]
self._project = None
@ -52,9 +54,15 @@ def update_frame_range(start, end, comp=None, set_render_range=True,
comp.SetAttrs(attrs)
def set_current_context_framerange():
def set_current_context_framerange(folder_entity=None):
"""Set Comp's frame range based on current folder."""
folder_entity = get_current_project_folder()
if folder_entity is None:
folder_entity = get_current_folder_entity(
fields={"attrib.frameStart",
"attrib.frameEnd",
"attrib.handleStart",
"attrib.handleEnd"})
folder_attributes = folder_entity["attrib"]
start = folder_attributes["frameStart"]
end = folder_attributes["frameEnd"]
@ -65,9 +73,24 @@ def set_current_context_framerange():
handle_end=handle_end)
def set_current_context_resolution():
def set_current_context_fps(folder_entity=None):
"""Set Comp's frame rate (FPS) to based on current asset"""
if folder_entity is None:
folder_entity = get_current_folder_entity(fields={"attrib.fps"})
fps = float(folder_entity["attrib"].get("fps", 24.0))
comp = get_current_comp()
comp.SetPrefs({
"Comp.FrameFormat.Rate": fps,
})
def set_current_context_resolution(folder_entity=None):
"""Set Comp's resolution width x height default based on current folder"""
folder_entity = get_current_project_folder()
if folder_entity is None:
folder_entity = get_current_folder_entity(
fields={"attrib.resolutionWidth", "attrib.resolutionHeight"})
folder_attributes = folder_entity["attrib"]
width = folder_attributes["resolutionWidth"]
height = folder_attributes["resolutionHeight"]
@ -101,7 +124,7 @@ def validate_comp_prefs(comp=None, force_repair=False):
"attrib.resolutionHeight",
"attrib.pixelAspect",
}
folder_entity = get_current_project_folder(fields=fields)
folder_entity = get_current_folder_entity(fields=fields)
folder_path = folder_entity["path"]
folder_attributes = folder_entity["attrib"]
@ -285,3 +308,98 @@ def comp_lock_and_undo_chunk(
finally:
comp.Unlock()
comp.EndUndo(keep_undo)
def update_content_on_context_change():
"""Update all Creator instances to current asset"""
host = registered_host()
context = host.get_current_context()
folder_path = context["folder_path"]
task = context["task_name"]
create_context = CreateContext(host, reset=True)
for instance in create_context.instances:
instance_folder_path = instance.get("folderPath")
if instance_folder_path and instance_folder_path != folder_path:
instance["folderPath"] = folder_path
instance_task = instance.get("task")
if instance_task and instance_task != task:
instance["task"] = task
create_context.save_changes()
def prompt_reset_context():
"""Prompt the user what context settings to reset.
This prompt is used on saving to a different task to allow the scene to
get matched to the new context.
"""
# TODO: Cleanup this prototyped mess of imports and odd dialog
from ayon_core.tools.attribute_defs.dialog import (
AttributeDefinitionsDialog
)
from ayon_core.style import load_stylesheet
from ayon_core.lib import BoolDef, UILabelDef
from qtpy import QtWidgets, QtCore
definitions = [
UILabelDef(
label=(
"You are saving your workfile into a different folder or task."
"\n\n"
"Would you like to update some settings to the new context?\n"
)
),
BoolDef(
"fps",
label="FPS",
tooltip="Reset Comp FPS",
default=True
),
BoolDef(
"frame_range",
label="Frame Range",
tooltip="Reset Comp start and end frame ranges",
default=True
),
BoolDef(
"resolution",
label="Comp Resolution",
tooltip="Reset Comp resolution",
default=True
),
BoolDef(
"instances",
label="Publish instances",
tooltip="Update all publish instance's folder and task to match "
"the new folder and task",
default=True
),
]
dialog = AttributeDefinitionsDialog(definitions)
dialog.setWindowFlags(
dialog.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
)
dialog.setWindowTitle("Saving to different context.")
dialog.setStyleSheet(load_stylesheet())
if not dialog.exec_():
return None
options = dialog.get_values()
folder_entity = get_current_folder_entity()
if options["frame_range"]:
set_current_context_framerange(folder_entity)
if options["fps"]:
set_current_context_fps(folder_entity)
if options["resolution"]:
set_current_context_resolution(folder_entity)
if options["instances"]:
update_content_on_context_change()
dialog.deleteLater()

View file

@ -5,6 +5,7 @@ import os
import sys
import logging
import contextlib
from pathlib import Path
import pyblish.api
from qtpy import QtCore
@ -28,8 +29,8 @@ from ayon_core.tools.utils import host_tools
from .lib import (
get_current_comp,
comp_lock_and_undo_chunk,
validate_comp_prefs
validate_comp_prefs,
prompt_reset_context
)
log = Logger.get_logger(__name__)
@ -41,6 +42,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
# Track whether the workfile tool is about to save
_about_to_save = False
class FusionLogHandler(logging.Handler):
# Keep a reference to fusion's Print function (Remote Object)
@ -104,8 +108,10 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
# Register events
register_event_callback("open", on_after_open)
register_event_callback("workfile.save.before", before_workfile_save)
register_event_callback("save", on_save)
register_event_callback("new", on_new)
register_event_callback("taskChanged", on_task_changed)
# region workfile io api
def has_unsaved_changes(self):
@ -169,6 +175,19 @@ def on_save(event):
comp = event["sender"]
validate_comp_prefs(comp)
# We are now starting the actual save directly
global _about_to_save
_about_to_save = False
def on_task_changed():
global _about_to_save
print(f"Task changed: {_about_to_save}")
# TODO: Only do this if not headless
if _about_to_save:
# Let's prompt the user to update the context settings or not
prompt_reset_context()
def on_after_open(event):
comp = event["sender"]
@ -202,6 +221,28 @@ def on_after_open(event):
dialog.setStyleSheet(load_stylesheet())
def before_workfile_save(event):
# Due to Fusion's external python process design we can't really
# detect whether the current Fusion environment matches the one the artists
# expects it to be. For example, our pipeline python process might
# have been shut down, and restarted - which will restart it to the
# environment Fusion started with; not necessarily where the artist
# is currently working.
# The `_about_to_save` var is used to detect context changes when
# saving into another asset. If we keep it False it will be ignored
# as context change. As such, before we change tasks we will only
# consider it the current filepath is within the currently known
# AVALON_WORKDIR. This way we avoid false positives of thinking it's
# saving to another context and instead sometimes just have false negatives
# where we fail to show the "Update on task change" prompt.
comp = get_current_comp()
filepath = comp.GetAttrs()["COMPS_FileName"]
workdir = os.environ.get("AYON_WORKDIR")
if Path(workdir) in Path(filepath).parents:
global _about_to_save
_about_to_save = True
def ls():
"""List containers from active Fusion scene
@ -338,7 +379,6 @@ class FusionEventHandler(QtCore.QObject):
>>> handler = FusionEventHandler(parent=window)
>>> handler.start()
"""
ACTION_IDS = [
"Comp_Save",

View file

@ -7,7 +7,7 @@ from ayon_core.hosts.fusion import (
FUSION_VERSIONS_DICT,
get_fusion_version,
)
from ayon_core.lib.applications import (
from ayon_applications import (
PreLaunchHook,
LaunchTypes,
ApplicationLaunchFailed,

View file

@ -1,5 +1,5 @@
import os
from ayon_core.lib.applications import (
from ayon_applications import (
PreLaunchHook,
LaunchTypes,
ApplicationLaunchFailed,

View file

@ -3,7 +3,7 @@ import subprocess
import platform
import uuid
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class InstallPySideToFusion(PreLaunchHook):

View file

@ -1,6 +1,11 @@
from ayon_core.lib import EnumDef
from ayon_core.lib import (
UILabelDef,
NumberDef,
EnumDef
)
from ayon_core.hosts.fusion.api.plugin import GenericCreateSaver
from ayon_core.hosts.fusion.api.lib import get_current_comp
class CreateSaver(GenericCreateSaver):
@ -45,6 +50,7 @@ class CreateSaver(GenericCreateSaver):
self._get_reviewable_bool(),
self._get_frame_range_enum(),
self._get_image_format_enum(),
*self._get_custom_frame_range_attribute_defs()
]
return attr_defs
@ -53,6 +59,7 @@ class CreateSaver(GenericCreateSaver):
"current_folder": "Current Folder context",
"render_range": "From render in/out",
"comp_range": "From composition timeline",
"custom_range": "Custom frame range",
}
return EnumDef(
@ -61,3 +68,82 @@ class CreateSaver(GenericCreateSaver):
label="Frame range source",
default=self.default_frame_range_option
)
@staticmethod
def _get_custom_frame_range_attribute_defs() -> list:
# Define custom frame range defaults based on current comp
# timeline settings (if a comp is currently open)
comp = get_current_comp()
if comp is not None:
attrs = comp.GetAttrs()
frame_defaults = {
"frameStart": int(attrs["COMPN_GlobalStart"]),
"frameEnd": int(attrs["COMPN_GlobalEnd"]),
"handleStart": int(
attrs["COMPN_RenderStart"] - attrs["COMPN_GlobalStart"]
),
"handleEnd": int(
attrs["COMPN_GlobalEnd"] - attrs["COMPN_RenderEnd"]
),
}
else:
frame_defaults = {
"frameStart": 1001,
"frameEnd": 1100,
"handleStart": 0,
"handleEnd": 0
}
return [
UILabelDef(
label="<br><b>Custom Frame Range</b><br>"
"<i>only used with 'Custom frame range' source</i>"
),
NumberDef(
"custom_frameStart",
label="Frame Start",
default=frame_defaults["frameStart"],
minimum=0,
decimals=0,
tooltip=(
"Set the start frame for the export.\n"
"Only used if frame range source is 'Custom frame range'."
)
),
NumberDef(
"custom_frameEnd",
label="Frame End",
default=frame_defaults["frameEnd"],
minimum=0,
decimals=0,
tooltip=(
"Set the end frame for the export.\n"
"Only used if frame range source is 'Custom frame range'."
)
),
NumberDef(
"custom_handleStart",
label="Handle Start",
default=frame_defaults["handleStart"],
minimum=0,
decimals=0,
tooltip=(
"Set the start handles for the export, this will be "
"added before the start frame.\n"
"Only used if frame range source is 'Custom frame range'."
)
),
NumberDef(
"custom_handleEnd",
label="Handle End",
default=frame_defaults["handleEnd"],
minimum=0,
decimals=0,
tooltip=(
"Set the end handles for the export, this will be added "
"after the end frame.\n"
"Only used if frame range source is 'Custom frame range'."
)
)
]

View file

@ -57,6 +57,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin):
start_with_handle = comp_start
end_with_handle = comp_end
if frame_range_source == "custom_range":
start = int(instance.data["custom_frameStart"])
end = int(instance.data["custom_frameEnd"])
handle_start = int(instance.data["custom_handleStart"])
handle_end = int(instance.data["custom_handleEnd"])
start_with_handle = start - handle_start
end_with_handle = end + handle_end
frame = instance.data["creator_attributes"].get("frame")
# explicitly publishing only single frame
if frame is not None:

View file

@ -568,7 +568,7 @@ def save_scene():
"""Save the Harmony scene safely.
The built-in (to Avalon) background zip and moving of the Harmony scene
folder, interfers with server/client communication by sending two requests
folder, interferes with server/client communication by sending two requests
at the same time. This only happens when sending "scene.saveAll()". This
method prevents this double request and safely saves the scene.

View file

@ -13,7 +13,7 @@ from ayon_core.pipeline import (
AVALON_CONTAINER_ID,
)
from ayon_core.pipeline.load import get_outdated_containers
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
from ayon_core.hosts.harmony import HARMONY_ADDON_ROOT
import ayon_core.hosts.harmony.api as harmony
@ -50,7 +50,7 @@ def get_current_context_settings():
"""
folder_entity = get_current_project_folder()
folder_entity = get_current_folder_entity()
folder_attributes = folder_entity["attrib"]
fps = folder_attributes.get("fps")

View file

@ -6,10 +6,7 @@ from ayon_core.lib import (
get_ayon_launcher_args,
is_using_ayon_console,
)
from ayon_core.lib.applications import (
PreLaunchHook,
LaunchTypes,
)
from ayon_applications import PreLaunchHook, LaunchTypes
from ayon_core.hosts.harmony import get_launch_script_path

View file

@ -21,12 +21,12 @@ class CreateFarmRender(plugin.Creator):
path = "render/{0}/{0}.".format(node.split("/")[-1])
harmony.send(
{
"function": f"PypeHarmony.Creators.CreateRender.create",
"function": "PypeHarmony.Creators.CreateRender.create",
"args": [node, path]
})
harmony.send(
{
"function": f"PypeHarmony.color",
"function": "PypeHarmony.color",
"args": [[0.9, 0.75, 0.3, 1.0]]
}
)

View file

@ -1,8 +1,8 @@
import os
import pyblish.api
import pyblish.api
class CollectAudio(pyblish.api.InstancePlugin):
"""
Collect relative path for audio file to instance.

View file

@ -17,7 +17,7 @@ class CollectScene(pyblish.api.ContextPlugin):
"""Plugin entry point."""
result = harmony.send(
{
f"function": "PypeHarmony.getSceneSettings",
"function": "PypeHarmony.getSceneSettings",
"args": []}
)["result"]
@ -62,7 +62,7 @@ class CollectScene(pyblish.api.ContextPlugin):
result = harmony.send(
{
f"function": "PypeHarmony.getVersion",
"function": "PypeHarmony.getVersion",
"args": []}
)["result"]
context.data["harmonyVersion"] = "{}.{}".format(result[0], result[1])

View file

@ -1,10 +1,12 @@
import os
import hiero.core.events
from ayon_core.lib import Logger, register_event_callback
from .lib import (
sync_avalon_data_to_workfile,
launch_workfiles_app,
selection_changed_timeline,
before_project_save,
)
from .tags import add_tags_to_workfile

View file

@ -166,7 +166,7 @@ def get_current_track(sequence, name, audio=False):
Creates new if none is found.
Args:
sequence (hiero.core.Sequence): hiero sequene object
sequence (hiero.core.Sequence): hiero sequence object
name (str): name of track we want to return
audio (bool)[optional]: switch to AudioTrack
@ -248,8 +248,12 @@ def get_track_items(
# collect all available active sequence track items
if not return_list:
sequence = get_current_sequence(name=sequence_name)
# get all available tracks from sequence
tracks = list(sequence.audioTracks()) + list(sequence.videoTracks())
tracks = []
if sequence is not None:
# get all available tracks from sequence
tracks.extend(sequence.audioTracks())
tracks.extend(sequence.videoTracks())
# loop all tracks
for track in tracks:
if check_locked and track.isLocked():
@ -846,8 +850,8 @@ def create_nuke_workfile_clips(nuke_workfiles, seq=None):
[{
'path': 'P:/Jakub_testy_pipeline/test_v01.nk',
'name': 'test',
'handleStart': 15, # added asymetrically to handles
'handleEnd': 10, # added asymetrically to handles
'handleStart': 15, # added asymmetrically to handles
'handleEnd': 10, # added asymmetrically to handles
"clipIn": 16,
"frameStart": 991,
"frameEnd": 1023,
@ -1192,7 +1196,7 @@ def get_sequence_pattern_and_padding(file):
Return:
string: any matching sequence pattern
int: padding of sequnce numbering
int: padding of sequence numbering
"""
foundall = re.findall(
r"(#+)|(%\d+d)|(?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$)", file)

View file

@ -90,7 +90,7 @@ def apply_transition(otio_track, otio_item, track):
if isinstance(track, hiero.core.AudioTrack):
kind = 'Audio'
# Gather TrackItems involved in trasition
# Gather TrackItems involved in transition
item_in, item_out = get_neighboring_trackitems(
otio_item,
otio_track,
@ -101,7 +101,7 @@ def apply_transition(otio_track, otio_item, track):
if transition_type == 'dissolve':
transition_func = getattr(
hiero.core.Transition,
'create{kind}DissolveTransition'.format(kind=kind)
"create{kind}DissolveTransition".format(kind=kind)
)
try:
@ -109,7 +109,7 @@ def apply_transition(otio_track, otio_item, track):
item_in,
item_out,
otio_item.in_offset.value,
otio_item.out_offset.value
otio_item.out_offset.value,
)
# Catch error raised if transition is bigger than TrackItem source
@ -134,7 +134,7 @@ def apply_transition(otio_track, otio_item, track):
transition = transition_func(
item_out,
otio_item.out_offset.value
otio_item.out_offset.value,
)
elif transition_type == 'fade_out':
@ -183,9 +183,7 @@ def prep_url(url_in):
def create_offline_mediasource(otio_clip, path=None):
global _otio_old
hiero_rate = hiero.core.TimeBase(
otio_clip.source_range.start_time.rate
)
hiero_rate = hiero.core.TimeBase(otio_clip.source_range.start_time.rate)
try:
legal_media_refs = (
@ -212,7 +210,7 @@ def create_offline_mediasource(otio_clip, path=None):
source_range.start_time.value,
source_range.duration.value,
hiero_rate,
source_range.start_time.value
source_range.start_time.value,
)
return media
@ -385,7 +383,8 @@ def create_trackitem(playhead, track, otio_clip, clip):
# Only reverse effect can be applied here
if abs(time_scalar) == 1.:
trackitem.setPlaybackSpeed(
trackitem.playbackSpeed() * time_scalar)
trackitem.playbackSpeed() * time_scalar
)
elif isinstance(effect, otio.schema.FreezeFrame):
# For freeze frame, playback speed must be set after range
@ -397,28 +396,21 @@ def create_trackitem(playhead, track, otio_clip, clip):
source_in = source_range.end_time_inclusive().value
timeline_in = playhead + source_out
timeline_out = (
timeline_in +
source_range.duration.value
) - 1
timeline_out = (timeline_in + source_range.duration.value) - 1
else:
# Normal playback speed
source_in = source_range.start_time.value
source_out = source_range.end_time_inclusive().value
timeline_in = playhead
timeline_out = (
timeline_in +
source_range.duration.value
) - 1
timeline_out = (timeline_in + source_range.duration.value) - 1
# Set source and timeline in/out points
trackitem.setTimes(
timeline_in,
timeline_out,
source_in,
source_out
source_out,
)
# Apply playback speed for freeze frames
@ -435,7 +427,8 @@ def create_trackitem(playhead, track, otio_clip, clip):
def build_sequence(
otio_timeline, project=None, sequence=None, track_kind=None):
otio_timeline, project=None, sequence=None, track_kind=None
):
if project is None:
if sequence:
project = sequence.project()
@ -509,10 +502,7 @@ def build_sequence(
# Create TrackItem
trackitem = create_trackitem(
playhead,
track,
otio_clip,
clip
playhead, track, otio_clip, clip
)
# Add markers

View file

@ -25,7 +25,7 @@ def get_reformated_path(path, padded=True):
path (str): path url or simple file name
Returns:
type: string with reformated path
type: string with reformatted path
Example:
get_reformated_path("plate.[0001-1008].exr") > plate.%04d.exr

View file

@ -449,7 +449,6 @@ class ClipLoader:
repr = self.context["representation"]
repr_cntx = repr["context"]
folder_path = self.context["folder"]["path"]
folder_name = self.context["folder"]["name"]
product_name = self.context["product"]["name"]
representation = repr["name"]
self.data["clip_name"] = self.clip_name_template.format(**repr_cntx)
@ -906,16 +905,16 @@ class PublishClip:
"hierarchyData": hierarchy_formatting_data,
"productName": self.product_name,
"productType": self.product_type,
"families": [self.product_type, self.data["family"]]
"families": [self.product_type, self.data["productType"]]
}
def _convert_to_entity(self, type, template):
def _convert_to_entity(self, src_type, template):
""" Converting input key to key with type. """
# convert to entity type
entity_type = self.types.get(type, None)
folder_type = self.types.get(src_type, None)
assert entity_type, "Missing entity type for `{}`".format(
type
assert folder_type, "Missing folder type for `{}`".format(
src_type
)
# first collect formatting data to use for formatting template
@ -926,7 +925,7 @@ class PublishClip:
formatting_data[_k] = value
return {
"entity_type": entity_type,
"folder_type": folder_type,
"entity_name": template.format(
**formatting_data
)

View file

@ -3,9 +3,11 @@
# Note: This only prints the text data that is visible in the active Spreadsheet View.
# If you've filtered text, only the visible text will be printed to the CSV file
# Usage: Copy to ~/.hiero/Python/StartupUI
import os
import csv
import hiero.core.events
import hiero.ui
import os, csv
try:
from PySide.QtGui import *
from PySide.QtCore import *

View file

@ -641,7 +641,7 @@ def _setStatus(self, status):
global gStatusTags
# Get a valid Tag object from the Global list of statuses
if not status in gStatusTags.keys():
if status not in gStatusTags.keys():
print("Status requested was not a valid Status string.")
return

View file

@ -90,7 +90,7 @@ def apply_transition(otio_track, otio_item, track):
kind = "Audio"
try:
# Gather TrackItems involved in trasition
# Gather TrackItems involved in transition
item_in, item_out = get_neighboring_trackitems(
otio_item,
otio_track,
@ -101,14 +101,14 @@ def apply_transition(otio_track, otio_item, track):
if transition_type == "dissolve":
transition_func = getattr(
hiero.core.Transition,
'create{kind}DissolveTransition'.format(kind=kind)
"create{kind}DissolveTransition".format(kind=kind)
)
transition = transition_func(
item_in,
item_out,
otio_item.in_offset.value,
otio_item.out_offset.value
otio_item.out_offset.value,
)
elif transition_type == "fade_in":
@ -116,20 +116,14 @@ def apply_transition(otio_track, otio_item, track):
hiero.core.Transition,
'create{kind}FadeInTransition'.format(kind=kind)
)
transition = transition_func(
item_out,
otio_item.out_offset.value
)
transition = transition_func(item_out, otio_item.out_offset.value)
elif transition_type == "fade_out":
transition_func = getattr(
hiero.core.Transition,
'create{kind}FadeOutTransition'.format(kind=kind)
)
transition = transition_func(
item_in,
otio_item.in_offset.value
"create{kind}FadeOutTransition".format(kind=kind)
)
transition = transition_func(item_in, otio_item.in_offset.value)
else:
# Unknown transition
@ -138,11 +132,10 @@ def apply_transition(otio_track, otio_item, track):
# Apply transition to track
track.addTransition(transition)
except Exception, e:
except Exception as e:
sys.stderr.write(
'Unable to apply transition "{t}": "{e}"\n'.format(
t=otio_item,
e=e
t=otio_item, e=e
)
)
@ -153,18 +146,14 @@ def prep_url(url_in):
if url.startswith("file://localhost/"):
return url.replace("file://localhost/", "")
url = '{url}'.format(
sep=url.startswith(os.sep) and "" or os.sep,
url=url.startswith(os.sep) and url[1:] or url
)
if url.startswith(os.sep):
url = url[1:]
return url
def create_offline_mediasource(otio_clip, path=None):
hiero_rate = hiero.core.TimeBase(
otio_clip.source_range.start_time.rate
)
hiero_rate = hiero.core.TimeBase(otio_clip.source_range.start_time.rate)
if isinstance(otio_clip.media_reference, otio.schema.ExternalReference):
source_range = otio_clip.available_range()
@ -180,7 +169,7 @@ def create_offline_mediasource(otio_clip, path=None):
source_range.start_time.value,
source_range.duration.value,
hiero_rate,
source_range.start_time.value
source_range.start_time.value,
)
return media
@ -203,7 +192,7 @@ marker_color_map = {
"MAGENTA": "Magenta",
"BLACK": "Blue",
"WHITE": "Green",
"MINT": "Cyan"
"MINT": "Cyan",
}
@ -254,12 +243,6 @@ def add_markers(otio_item, hiero_item, tagsbin):
if _tag is None:
_tag = hiero.core.Tag(marker_color_map[marker.color])
start = marker.marked_range.start_time.value
end = (
marker.marked_range.start_time.value +
marker.marked_range.duration.value
)
tag = hiero_item.addTag(_tag)
tag.setName(marker.name or marker_color_map[marker_color])
@ -275,12 +258,12 @@ def create_track(otio_track, tracknum, track_kind):
# Create a Track
if otio_track.kind == otio.schema.TrackKind.Video:
track = hiero.core.VideoTrack(
otio_track.name or 'Video{n}'.format(n=tracknum)
otio_track.name or "Video{n}".format(n=tracknum)
)
else:
track = hiero.core.AudioTrack(
otio_track.name or 'Audio{n}'.format(n=tracknum)
otio_track.name or "Audio{n}".format(n=tracknum)
)
return track
@ -315,34 +298,25 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin):
for effect in otio_clip.effects:
if isinstance(effect, otio.schema.LinearTimeWarp):
trackitem.setPlaybackSpeed(
trackitem.playbackSpeed() *
effect.time_scalar
trackitem.playbackSpeed() * effect.time_scalar
)
# If reverse playback speed swap source in and out
if trackitem.playbackSpeed() < 0:
source_out = source_range.start_time.value
source_in = (
source_range.start_time.value +
source_range.duration.value
source_range.start_time.value + source_range.duration.value
) - 1
timeline_in = playhead + source_out
timeline_out = (
timeline_in +
source_range.duration.value
) - 1
timeline_out = (timeline_in + source_range.duration.value) - 1
else:
# Normal playback speed
source_in = source_range.start_time.value
source_out = (
source_range.start_time.value +
source_range.duration.value
source_range.start_time.value + source_range.duration.value
) - 1
timeline_in = playhead
timeline_out = (
timeline_in +
source_range.duration.value
) - 1
timeline_out = (timeline_in + source_range.duration.value) - 1
# Set source and timeline in/out points
trackitem.setSourceIn(source_in)
@ -357,7 +331,8 @@ def create_trackitem(playhead, track, otio_clip, clip, tagsbin):
def build_sequence(
otio_timeline, project=None, sequence=None, track_kind=None):
otio_timeline, project=None, sequence=None, track_kind=None
):
if project is None:
if sequence:
@ -414,8 +389,7 @@ def build_sequence(
if isinstance(otio_clip, otio.schema.Stack):
bar = hiero.ui.mainWindow().statusBar()
bar.showMessage(
"Nested sequences are created separately.",
timeout=3000
"Nested sequences are created separately.", timeout=3000
)
build_sequence(otio_clip, project, otio_track.kind)
@ -428,11 +402,7 @@ def build_sequence(
# Create TrackItem
trackitem = create_trackitem(
playhead,
track,
otio_clip,
clip,
tagsbin
playhead, track, otio_clip, clip, tagsbin
)
# Add trackitem to track

View file

@ -89,7 +89,7 @@ def update_tag(tag, data):
# set all data metadata to tag metadata
for _k, _v in data_mtd.items():
value = str(_v)
if type(_v) == dict:
if isinstance(_v, dict):
value = json.dumps(_v)
# set the value

View file

@ -166,7 +166,7 @@ class CreateShotClip(phiero.Creator):
"type": "QCheckBox",
"label": "Source resolution",
"target": "tag",
"toolTip": "Is resloution taken from timeline or source?", # noqa
"toolTip": "Is resolution taken from timeline or source?", # noqa
"order": 4},
}
},
@ -211,7 +211,7 @@ class CreateShotClip(phiero.Creator):
presets = deepcopy(self.presets)
gui_inputs = deepcopy(self.gui_inputs)
# get key pares from presets and match it on ui inputs
# get key pairs from presets and match it on ui inputs
for k, v in gui_inputs.items():
if v["type"] in ("dict", "section"):
# nested dictionary (only one level allowed

View file

@ -1,5 +1,5 @@
from itertools import product
import re
import pyblish.api

View file

@ -43,7 +43,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
tracks_effect_items = self.collect_sub_track_items(all_tracks)
context.data["tracksEffectItems"] = tracks_effect_items
# process all sellected timeline track items
# process all selected timeline track items
for track_item in selected_timeline_items:
data = {}
clip_name = track_item.name()
@ -62,7 +62,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
}:
continue
# get clips subtracks and anotations
# get clips subtracks and annotations
annotations = self.clip_annotations(source_clip)
subtracks = self.clip_subtrack(track_item)
self.log.debug("Annotations: {}".format(annotations))
@ -84,8 +84,13 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
k: v for k, v in tag_data.items()
if k not in ("id", "applieswhole", "label")
})
# Backward compatibility fix of 'entity_type' > 'folder_type'
if "parents" in data:
for parent in data["parents"]:
if "entity_type" in parent:
parent["folder_type"] = parent.pop("entity_type")
asset, asset_name = self._get_folder_data(tag_data)
folder_path, folder_name = self._get_folder_data(tag_data)
product_name = tag_data.get("productName")
if product_name is None:
@ -93,12 +98,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
families = [str(f) for f in tag_data["families"]]
# form label
label = "{} -".format(asset)
if asset_name != clip_name:
label += " ({})".format(clip_name)
label += " {}".format(product_name)
# TODO: remove backward compatibility
product_name = tag_data.get("productName")
if product_name is None:
@ -108,7 +107,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# backward compatibility: product_name should not be missing
if not product_name:
self.log.error(
"Product name is not defined for: {}".format(asset))
"Product name is not defined for: {}".format(folder_path))
# TODO: remove backward compatibility
product_type = tag_data.get("productType")
@ -119,15 +118,21 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# backward compatibility: product_type should not be missing
if not product_type:
self.log.error(
"Product type is not defined for: {}".format(asset))
"Product type is not defined for: {}".format(folder_path))
# form label
label = "{} -".format(folder_path)
if folder_name != clip_name:
label += " ({})".format(clip_name)
label += " {}".format(product_name)
data.update({
"name": "{}_{}".format(asset, product_name),
"name": "{}_{}".format(folder_path, product_name),
"label": label,
"folderPath": asset,
"asset_name": asset_name,
"productName": product_name,
"productType": product_type,
"folderPath": folder_path,
"asset_name": folder_name,
"item": track_item,
"families": families,
"publish": tag_data["publish"],
@ -217,19 +222,19 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
if not hierarchy_data:
return
asset = data["folderPath"]
asset_name = data["asset_name"]
folder_path = data["folderPath"]
folder_name = data["asset_name"]
product_type = "shot"
# form label
label = "{} -".format(asset)
if asset_name != clip_name:
label = "{} -".format(folder_path)
if folder_name != clip_name:
label += " ({}) ".format(clip_name)
label += " {}".format(product_name)
data.update({
"name": "{}_{}".format(asset, product_name),
"name": "{}_{}".format(folder_path, product_name),
"label": label,
"productName": product_name,
"productType": product_type,
@ -276,19 +281,19 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
if not self.test_any_audio(item):
return
asset = data["folderPath"]
folder_path = data["folderPath"]
asset_name = data["asset_name"]
product_type = "audio"
# form label
label = "{} -".format(asset)
label = "{} -".format(folder_path)
if asset_name != clip_name:
label += " ({}) ".format(clip_name)
label += " {}".format(product_name)
data.update({
"name": "{}_{}".format(asset, product_name),
"name": "{}_{}".format(folder_path, subset),
"label": label,
"productName": product_name,
"productType": product_type,
@ -378,12 +383,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# collect all subtrack items
sub_track_items = {}
for track in tracks:
items = track.items()
effet_items = track.subTrackItems()
effect_items = track.subTrackItems()
# skip if no clips on track > need track with effect only
if not effet_items:
if not effect_items:
continue
# skip all disabled tracks
@ -391,7 +394,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
continue
track_index = track.trackIndex()
_sub_track_items = phiero.flatten(effet_items)
_sub_track_items = phiero.flatten(effect_items)
_sub_track_items = list(_sub_track_items)
# continue only if any subtrack items are collected
@ -439,10 +442,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
for item in subTrackItems:
if "TimeWarp" in item.name():
continue
# avoid all anotation
# avoid all annotation
if isinstance(item, hiero.core.Annotation):
continue
# # avoid all not anaibled
# avoid all disabled
if not item.isEnabled():
continue
subtracks.append(item)

View file

@ -17,8 +17,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
order = pyblish.api.CollectorOrder - 0.491
def process(self, context):
asset = context.data["folderPath"]
asset_name = asset.split("/")[-1]
folder_path = context.data["folderPath"]
folder_name = folder_path.split("/")[-1]
active_timeline = hiero.ui.activeSequence()
project = active_timeline.project()
@ -62,12 +62,12 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin):
product_type = "workfile"
instance_data = {
"label": "{} - {}Main".format(
asset, product_type),
"name": "{}_{}".format(asset_name, product_type),
"folderPath": context.data["folderPath"],
folder_path, product_type),
"name": "{}_{}".format(folder_name, product_type),
"folderPath": folder_path,
# TODO use 'get_product_name'
"productName": "{}{}Main".format(
asset_name, product_type.capitalize()
folder_name, product_type.capitalize()
),
"item": project,
"productType": product_type,

View file

@ -35,10 +35,6 @@ class PrecollectRetime(api.InstancePlugin):
source_out = int(track_item.sourceOut())
speed = track_item.playbackSpeed()
# calculate available material before retime
available_in = int(track_item.handleInLength() * speed)
available_out = int(track_item.handleOutLength() * speed)
self.log.debug((
"_BEFORE: \n timeline_in: `{0}`,\n timeline_out: `{1}`, \n "
"source_in: `{2}`,\n source_out: `{3}`,\n speed: `{4}`,\n "

View file

@ -91,7 +91,7 @@ def create_interactive(creator_identifier, **kwargs):
pane = stateutils.activePane(kwargs)
if isinstance(pane, hou.NetworkEditor):
pwd = pane.pwd()
project_name = context.get_current_project_name(),
project_name = context.get_current_project_name()
folder_path = context.get_current_folder_path()
task_name = context.get_current_task_name()
folder_entity = ayon_api.get_folder_by_path(

View file

@ -3,7 +3,6 @@ import sys
import os
import errno
import re
import uuid
import logging
import json
from contextlib import contextmanager
@ -23,7 +22,7 @@ from ayon_core.pipeline import (
)
from ayon_core.pipeline.create import CreateContext
from ayon_core.pipeline.template_data import get_template_data
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup
from ayon_core.tools.utils.host_tools import get_tool_by_name
@ -40,88 +39,10 @@ def get_folder_fps(folder_entity=None):
"""Return current folder fps."""
if folder_entity is None:
folder_entity = get_current_project_folder(fields=["attrib.fps"])
folder_entity = get_current_folder_entity(fields=["attrib.fps"])
return folder_entity["attrib"]["fps"]
def set_id(node, unique_id, overwrite=False):
exists = node.parm("id")
if not exists:
imprint(node, {"id": unique_id})
if not exists and overwrite:
node.setParm("id", unique_id)
def get_id(node):
"""Get the `cbId` attribute of the given node.
Args:
node (hou.Node): the name of the node to retrieve the attribute from
Returns:
str: cbId attribute of the node.
"""
if node is not None:
return node.parm("id")
def generate_ids(nodes, folder_id=None):
"""Returns new unique ids for the given nodes.
Note: This does not assign the new ids, it only generates the values.
To assign new ids using this method:
>>> nodes = ["a", "b", "c"]
>>> for node, id in generate_ids(nodes):
>>> set_id(node, id)
To also override any existing values (and assign regenerated ids):
>>> nodes = ["a", "b", "c"]
>>> for node, id in generate_ids(nodes):
>>> set_id(node, id, overwrite=True)
Args:
nodes (list): List of nodes.
folder_id (str): Folder id . Use current folder id if is ``None``.
Returns:
list: A list of (node, id) tuples.
"""
if folder_id is None:
project_name = get_current_project_name()
folder_path = get_current_folder_path()
# Get folder id of current context folder
folder_entity = ayon_api.get_folder_by_path(
project_name, folder_path, fields={"id"}
)
if not folder_entity:
raise ValueError("No current folder is set.")
folder_id = folder_entity["id"]
node_ids = []
for node in nodes:
_, uid = str(uuid.uuid4()).rsplit("-", 1)
unique_id = "{}:{}".format(folder_id, uid)
node_ids.append((node, unique_id))
return node_ids
def get_id_required_nodes():
valid_types = ["geometry"]
nodes = {n for n in hou.node("/out").children() if
n.type().name() in valid_types}
return list(nodes)
def get_output_parameter(node):
"""Return the render output parameter of the given node
@ -322,7 +243,10 @@ def render_rop(ropnode):
try:
ropnode.render(verbose=verbose,
# Allow Deadline to capture completion percentage
output_progress=verbose)
output_progress=verbose,
# Render only this node
# (do not render any of its dependencies)
ignore_inputs=True)
except hou.Error as exc:
# The hou.Error is not inherited from a Python Exception class,
# so we explicitly capture the houdini error, otherwise pyblish
@ -526,7 +450,7 @@ def maintained_selection():
node.setSelected(on=True)
def reset_framerange():
def reset_framerange(fps=True, frame_range=True):
"""Set frame range and FPS to current folder."""
project_name = get_current_project_name()
@ -535,29 +459,32 @@ def reset_framerange():
folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
folder_attributes = folder_entity["attrib"]
# Get FPS
fps = get_folder_fps(folder_entity)
# Set FPS
if fps:
fps = get_folder_fps(folder_entity)
print("Setting scene FPS to {}".format(int(fps)))
set_scene_fps(fps)
# Get Start and End Frames
frame_start = folder_attributes.get("frameStart")
frame_end = folder_attributes.get("frameEnd")
if frame_range:
if frame_start is None or frame_end is None:
log.warning("No edit information found for '{}'".format(folder_path))
return
# Set Start and End Frames
frame_start = folder_attributes.get("frameStart")
frame_end = folder_attributes.get("frameEnd")
handle_start = folder_attributes.get("handleStart", 0)
handle_end = folder_attributes.get("handleEnd", 0)
if frame_start is None or frame_end is None:
log.warning("No edit information found for '%s'", folder_path)
return
frame_start -= int(handle_start)
frame_end += int(handle_end)
handle_start = folder_attributes.get("handleStart", 0)
handle_end = folder_attributes.get("handleEnd", 0)
# Set frame range and FPS
print("Setting scene FPS to {}".format(int(fps)))
set_scene_fps(fps)
hou.playbar.setFrameRange(frame_start, frame_end)
hou.playbar.setPlaybackRange(frame_start, frame_end)
hou.setFrame(frame_start)
frame_start -= int(handle_start)
frame_end += int(handle_end)
# Set frame range and FPS
hou.playbar.setFrameRange(frame_start, frame_end)
hou.playbar.setPlaybackRange(frame_start, frame_end)
hou.setFrame(frame_start)
def get_main_window():
@ -814,7 +741,7 @@ def set_camera_resolution(camera, folder_entity=None):
"""Apply resolution to camera from folder entity of the publish"""
if not folder_entity:
folder_entity = get_current_project_folder()
folder_entity = get_current_folder_entity()
resolution = get_resolution_from_folder(folder_entity)
@ -1024,7 +951,7 @@ def self_publish():
Firstly, it gets the node and its dependencies.
Then, it deactivates all other ROPs
And finaly, it triggers the publishing action.
And finally, it triggers the publishing action.
"""
result, comment = hou.ui.readInput(
@ -1072,3 +999,84 @@ def add_self_publish_button(node):
template = node.parmTemplateGroup()
template.insertBefore((0,), button_parm)
node.setParmTemplateGroup(template)
def update_content_on_context_change():
"""Update all Creator instances to current asset"""
host = registered_host()
context = host.get_current_context()
folder_path = context["folder_path"]
task = context["task_name"]
create_context = CreateContext(host, reset=True)
for instance in create_context.instances:
instance_folder_path = instance.get("folderPath")
if instance_folder_path and instance_folder_path != folder_path:
instance["folderPath"] = folder_path
instance_task = instance.get("task")
if instance_task and instance_task != task:
instance["task"] = task
create_context.save_changes()
def prompt_reset_context():
"""Prompt the user what context settings to reset.
This prompt is used on saving to a different task to allow the scene to
get matched to the new context.
"""
# TODO: Cleanup this prototyped mess of imports and odd dialog
from ayon_core.tools.attribute_defs.dialog import (
AttributeDefinitionsDialog
)
from ayon_core.style import load_stylesheet
from ayon_core.lib import BoolDef, UILabelDef
definitions = [
UILabelDef(
label=(
"You are saving your workfile into a different folder or task."
"\n\n"
"Would you like to update some settings to the new context?\n"
)
),
BoolDef(
"fps",
label="FPS",
tooltip="Reset workfile FPS",
default=True
),
BoolDef(
"frame_range",
label="Frame Range",
tooltip="Reset workfile start and end frame ranges",
default=True
),
BoolDef(
"instances",
label="Publish instances",
tooltip="Update all publish instance's folder and task to match "
"the new folder and task",
default=True
),
]
dialog = AttributeDefinitionsDialog(definitions)
dialog.setWindowTitle("Saving to different context.")
dialog.setStyleSheet(load_stylesheet())
if not dialog.exec_():
return None
options = dialog.get_values()
if options["fps"] or options["frame_range"]:
reset_framerange(
fps=options["fps"],
frame_range=options["frame_range"]
)
if options["instances"]:
update_content_on_context_change()
dialog.deleteLater()

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
"""Pipeline tools for OpenPype Houdini integration."""
import os
import sys
import logging
import hou # noqa
@ -39,6 +38,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
# Track whether the workfile tool is about to save
_about_to_save = False
class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "houdini"
@ -61,10 +63,12 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
log.info("Installing callbacks ... ")
# register_event_callback("init", on_init)
self._register_callbacks()
register_event_callback("workfile.save.before", before_workfile_save)
register_event_callback("before.save", before_save)
register_event_callback("save", on_save)
register_event_callback("open", on_open)
register_event_callback("new", on_new)
register_event_callback("taskChanged", on_task_changed)
self._has_been_setup = True
@ -166,7 +170,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
if not op_ctx:
op_ctx = self.create_context_node()
lib.imprint(op_ctx, data)
lib.imprint(op_ctx, data, update=True)
def get_context_data(self):
op_ctx = hou.node(CONTEXT_CONTAINER)
@ -287,6 +291,11 @@ def ls():
yield parse_container(container)
def before_workfile_save(event):
global _about_to_save
_about_to_save = True
def before_save():
return lib.validate_fps()
@ -298,9 +307,16 @@ def on_save():
# update houdini vars
lib.update_houdini_vars_context_dialog()
nodes = lib.get_id_required_nodes()
for node, new_id in lib.generate_ids(nodes):
lib.set_id(node, new_id, overwrite=False)
# We are now starting the actual save directly
global _about_to_save
_about_to_save = False
def on_task_changed():
global _about_to_save
if not IS_HEADLESS and _about_to_save:
# Let's prompt the user to update the context settings or not
lib.prompt_reset_context()
def _show_outdated_content_popup():

View file

@ -1,4 +1,4 @@
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class SetPath(PreLaunchHook):

View file

@ -15,6 +15,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
product_type = "redshift_rop"
icon = "magic"
ext = "exr"
multi_layered_mode = "No Multi-Layered EXR File"
# Default to split export and render jobs
split_render = True
@ -55,25 +56,36 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
# Set the linked rop to the Redshift ROP
ipr_rop.parm("linked_rop").set(instance_node.path())
ext = pre_create_data.get("image_format")
filepath = "{renders_dir}{product_name}/{product_name}.{fmt}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
product_name=product_name,
fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext)
)
multi_layered_mode = pre_create_data.get("multi_layered_mode")
ext_format_index = {"exr": 0, "tif": 1, "jpg": 2, "png": 3}
multilayer_mode_index = {"No Multi-Layered EXR File": "1",
"Full Multi-Layered EXR File": "2" }
filepath = "{renders_dir}{product_name}/{product_name}.{fmt}".format(
renders_dir=hou.text.expandString("$HIP/pyblish/renders/"),
product_name=product_name,
fmt="$AOV.$F4.{ext}".format(ext=ext)
)
if multilayer_mode_index[multi_layered_mode] == "1":
multipart = False
elif multilayer_mode_index[multi_layered_mode] == "2":
multipart = True
parms = {
# Render frame range
"trange": 1,
# Redshift ROP settings
"RS_outputFileNamePrefix": filepath,
"RS_outputMultilayerMode": "1", # no multi-layered exr
"RS_outputBeautyAOVSuffix": "beauty",
"RS_outputFileFormat": ext_format_index[ext],
}
if ext == "exr":
parms["RS_outputMultilayerMode"] = multilayer_mode_index[multi_layered_mode]
parms["RS_aovMultipart"] = multipart
if self.selected_nodes:
# set up the render camera from the selected node
@ -111,6 +123,11 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
image_format_enum = [
"exr", "tif", "jpg", "png",
]
multi_layered_mode = [
"No Multi-Layered EXR File",
"Full Multi-Layered EXR File"
]
return attrs + [
BoolDef("farm",
@ -122,5 +139,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator):
EnumDef("image_format",
image_format_enum,
default=self.ext,
label="Image Format Options")
label="Image Format Options"),
EnumDef("multi_layered_mode",
multi_layered_mode,
default=self.multi_layered_mode,
label="Multi-Layered EXR")
]

View file

@ -3,7 +3,7 @@ from ayon_core.hosts.houdini.api.lib import (
get_camera_from_container,
set_camera_resolution
)
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
class SetCameraResolution(InventoryAction):
@ -19,7 +19,7 @@ class SetCameraResolution(InventoryAction):
)
def process(self, containers):
folder_entity = get_current_project_folder()
folder_entity = get_current_folder_entity()
for container in containers:
node = container["node"]
camera = get_camera_from_container(node)

View file

@ -76,8 +76,8 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin):
return
# Include handles
start -= version_data.get("handleStart", 0)
end += version_data.get("handleEnd", 0)
start -= version_attributes.get("handleStart", 0)
end += version_attributes.get("handleEnd", 0)
hou.playbar.setFrameRange(start, end)
hou.playbar.setPlaybackRange(start, end)

View file

@ -59,7 +59,7 @@ class AbcLoader(load.LoaderPlugin):
normal_node.setInput(0, unpack)
null = container.createNode("null", node_name="OUT".format(name))
null = container.createNode("null", node_name="OUT")
null.setInput(0, normal_node)
# Ensure display flag is on the Alembic input node and not on the OUT

View file

@ -167,6 +167,9 @@ class CameraLoader(load.LoaderPlugin):
temp_camera.destroy()
def switch(self, container, context):
self.update(container, context)
def remove(self, container):
node = container["node"]
@ -195,7 +198,6 @@ class CameraLoader(load.LoaderPlugin):
def _match_maya_render_mask(self, camera):
"""Workaround to match Maya render mask in Houdini"""
# print("Setting match maya render mask ")
parm = camera.parm("aperture")
expression = parm.expression()
expression = expression.replace("return ", "aperture = ")

View file

@ -0,0 +1,129 @@
import os
import re
from ayon_core.pipeline import load
from ayon_core.hosts.houdini.api import pipeline
import hou
class FilePathLoader(load.LoaderPlugin):
"""Load a managed filepath to a null node.
This is useful if for a particular workflow there is no existing loader
yet. A Houdini artists can load as the generic filepath loader and then
reference the relevant Houdini parm to use the exact value. The benefit
is that this filepath will be managed and can be updated as usual.
"""
label = "Load filepath to node"
order = 9
icon = "link"
color = "white"
product_types = {"*"}
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
# Get the root node
obj = hou.node("/obj")
# Define node name
namespace = namespace if namespace else context["folder"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a null node
container = obj.createNode("null", node_name=node_name)
# Destroy any children
for node in container.children():
node.destroy()
# Add filepath attribute, set value as default value
filepath = self.format_path(
path=self.filepath_from_context(context),
representation=context["representation"]
)
parm_template_group = container.parmTemplateGroup()
attr_folder = hou.FolderParmTemplate("attributes_folder", "Attributes")
parm = hou.StringParmTemplate(name="filepath",
label="Filepath",
num_components=1,
default_value=(filepath,))
attr_folder.addParmTemplate(parm)
parm_template_group.append(attr_folder)
# Hide some default labels
for folder_label in ["Transform", "Render", "Misc", "Redshift OBJ"]:
folder = parm_template_group.findFolder(folder_label)
if not folder:
continue
parm_template_group.hideFolder(folder_label, True)
container.setParmTemplateGroup(parm_template_group)
container.setDisplayFlag(False)
container.setSelectableInViewport(False)
container.useXray(False)
nodes = [container]
self[:] = nodes
return pipeline.containerise(
node_name,
namespace,
nodes,
context,
self.__class__.__name__,
suffix="",
)
def update(self, container, context):
# Update the file path
representation_entity = context["representation"]
file_path = self.format_path(
path=self.filepath_from_context(context),
representation=representation_entity
)
node = container["node"]
node.setParms({
"filepath": file_path,
"representation": str(representation_entity["id"])
})
# Update the parameter default value (cosmetics)
parm_template_group = node.parmTemplateGroup()
parm = parm_template_group.find("filepath")
parm.setDefaultValue((file_path,))
parm_template_group.replace(parm_template_group.find("filepath"),
parm)
node.setParmTemplateGroup(parm_template_group)
def switch(self, container, context):
self.update(container, context)
def remove(self, container):
node = container["node"]
node.destroy()
@staticmethod
def format_path(path: str, representation: dict) -> str:
"""Format file path for sequence with $F."""
if not os.path.exists(path):
raise RuntimeError("Path does not exist: %s" % path)
# The path is either a single file or sequence in a folder.
frame = representation["context"].get("frame")
if frame is not None:
# Substitute frame number in sequence with $F with padding
ext = representation.get("ext", representation["name"])
token = "$F{}".format(len(frame)) # e.g. $F4
pattern = r"\.(\d+)\.{ext}$".format(ext=re.escape(ext))
path = re.sub(pattern, ".{}.{}".format(token, ext), path)
return os.path.normpath(path).replace("\\", "/")

View file

@ -0,0 +1,77 @@
import os
from ayon_core.pipeline import load
from ayon_core.hosts.houdini.api import pipeline
class SopUsdImportLoader(load.LoaderPlugin):
"""Load USD to SOPs via `usdimport`"""
label = "Load USD to SOPs"
product_types = {"*"}
representations = ["usd"]
order = -6
icon = "code-fork"
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
import hou
# Format file name, Houdini only wants forward slashes
file_path = self.filepath_from_context(context)
file_path = os.path.normpath(file_path)
file_path = file_path.replace("\\", "/")
# Get the root node
obj = hou.node("/obj")
# Define node name
namespace = namespace if namespace else context["folder"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a new geo node
container = obj.createNode("geo", node_name=node_name)
# Create a usdimport node
usdimport = container.createNode("usdimport", node_name=node_name)
usdimport.setParms({"filepath1": file_path})
# Set new position for unpack node else it gets cluttered
nodes = [container, usdimport]
return pipeline.containerise(
node_name,
namespace,
nodes,
context,
self.__class__.__name__,
suffix="",
)
def update(self, container, context):
node = container["node"]
try:
usdimport_node = next(
n for n in node.children() if n.type().name() == "usdimport"
)
except StopIteration:
self.log.error("Could not find node of type `usdimport`")
return
# Update the file path
file_path = self.filepath_from_context(context)
file_path = file_path.replace("\\", "/")
usdimport_node.setParms({"filepath1": file_path})
# Update attribute
node.setParms({"representation": context["representation"]["id"]})
def remove(self, container):
node = container["node"]
node.destroy()
def switch(self, container, representation):
self.update(container, representation)

View file

@ -41,23 +41,23 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin):
instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "picture")
render_products = []
default_prefix = evalParmNoFrame(rop, "picture")
render_products = []
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
)
render_products.append(beauty_product)
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
)
render_products.append(beauty_product)
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()
filenames = list(render_products)
instance.data["files"] = filenames
instance.data["renderProducts"] = colorspace.ARenderProduct()
for product in render_products:
self.log.debug("Found render product: %s" % product)

View file

@ -41,57 +41,57 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin):
instance.data["chunkSize"] = chunk_size
self.log.debug("Chunk Size: %s" % chunk_size)
default_prefix = evalParmNoFrame(rop, "vm_picture")
render_products = []
default_prefix = evalParmNoFrame(rop, "vm_picture")
render_products = []
# Store whether we are splitting the render job (export + render)
split_render = bool(rop.parm("soho_outputmode").eval())
instance.data["splitRender"] = split_render
export_prefix = None
export_products = []
if split_render:
export_prefix = evalParmNoFrame(
rop, "soho_diskfile", pad_character="0"
)
beauty_export_product = self.get_render_product_name(
prefix=export_prefix,
suffix=None)
export_products.append(beauty_export_product)
self.log.debug(
"Found export product: {}".format(beauty_export_product)
)
instance.data["ifdFile"] = beauty_export_product
instance.data["exportFiles"] = list(export_products)
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
# Store whether we are splitting the render job (export + render)
split_render = bool(rop.parm("soho_outputmode").eval())
instance.data["splitRender"] = split_render
export_prefix = None
export_products = []
if split_render:
export_prefix = evalParmNoFrame(
rop, "soho_diskfile", pad_character="0"
)
render_products.append(beauty_product)
beauty_export_product = self.get_render_product_name(
prefix=export_prefix,
suffix=None)
export_products.append(beauty_export_product)
self.log.debug(
"Found export product: {}".format(beauty_export_product)
)
instance.data["ifdFile"] = beauty_export_product
instance.data["exportFiles"] = list(export_products)
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
# Default beauty AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=None
)
render_products.append(beauty_product)
aov_numbers = rop.evalParm("vm_numaux")
if aov_numbers > 0:
# get the filenames of the AOVs
for i in range(1, aov_numbers + 1):
var = rop.evalParm("vm_variable_plane%d" % i)
if var:
aov_name = "vm_filename_plane%d" % i
aov_boolean = "vm_usefile_plane%d" % i
aov_enabled = rop.evalParm(aov_boolean)
has_aov_path = rop.evalParm(aov_name)
if has_aov_path and aov_enabled == 1:
aov_prefix = evalParmNoFrame(rop, aov_name)
aov_product = self.get_render_product_name(
prefix=aov_prefix, suffix=None
)
render_products.append(aov_product)
files_by_aov = {
"beauty": self.generate_expected_files(instance,
beauty_product)
}
files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa
aov_numbers = rop.evalParm("vm_numaux")
if aov_numbers > 0:
# get the filenames of the AOVs
for i in range(1, aov_numbers + 1):
var = rop.evalParm("vm_variable_plane%d" % i)
if var:
aov_name = "vm_filename_plane%d" % i
aov_boolean = "vm_usefile_plane%d" % i
aov_enabled = rop.evalParm(aov_boolean)
has_aov_path = rop.evalParm(aov_name)
if has_aov_path and aov_enabled == 1:
aov_prefix = evalParmNoFrame(rop, aov_name)
aov_product = self.get_render_product_name(
prefix=aov_prefix, suffix=None
)
render_products.append(aov_product)
files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa
for product in render_products:
self.log.debug("Found render product: %s" % product)

View file

@ -60,15 +60,22 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
instance.data["ifdFile"] = beauty_export_product
instance.data["exportFiles"] = list(export_products)
# Default beauty AOV
full_exr_mode = (rop.evalParm("RS_outputMultilayerMode") == "2")
if full_exr_mode:
# Ignore beauty suffix if full mode is enabled
# As this is what the rop does.
beauty_suffix = ""
# Default beauty/main layer AOV
beauty_product = self.get_render_product_name(
prefix=default_prefix, suffix=beauty_suffix
)
render_products = [beauty_product]
files_by_aov = {
"_": self.generate_expected_files(instance,
beauty_product)}
beauty_suffix: self.generate_expected_files(instance,
beauty_product)
}
aovs_rop = rop.parm("RS_aovGetFromNode").evalAsNode()
if aovs_rop:
rop = aovs_rop
@ -89,11 +96,14 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
if not aov_prefix:
aov_prefix = default_prefix
aov_product = self.get_render_product_name(aov_prefix, aov_suffix)
render_products.append(aov_product)
if rop.parm(f"RS_aovID_{i}").evalAsString() == "CRYPTOMATTE" or \
not full_exr_mode:
aov_product = self.get_render_product_name(aov_prefix, aov_suffix)
render_products.append(aov_product)
files_by_aov[aov_suffix] = self.generate_expected_files(instance,
aov_product) # noqa
files_by_aov[aov_suffix] = self.generate_expected_files(instance,
aov_product) # noqa
for product in render_products:
self.log.debug("Found render product: %s" % product)
@ -121,7 +131,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin):
# When AOV is explicitly defined in prefix we just swap it out
# directly with the AOV suffix to embed it.
# Note: ${AOV} seems to be evaluated in the parameter as %AOV%
# Note: '$AOV' seems to be evaluated in the parameter as '%AOV%'
has_aov_in_prefix = "%AOV%" in prefix
if has_aov_in_prefix:
# It seems that when some special separator characters are present

View file

@ -71,6 +71,8 @@ class ValidateCopOutputNode(pyblish.api.InstancePlugin):
# the isinstance check above should be stricter than this category
if output_node.type().category().name() != "Cop2":
raise PublishValidationError(
("Output node %s is not of category Cop2. "
"This is a bug...").format(output_node.path()),
(
"Output node {} is not of category Cop2."
" This is a bug..."
).format(output_node.path()),
title=cls.label)

View file

@ -11,7 +11,7 @@ import ayon_api
from ayon_core.pipeline import get_current_project_name, colorspace
from ayon_core.settings import get_project_settings
from ayon_core.pipeline.context_tools import (
get_current_project_folder,
get_current_folder_entity,
)
from ayon_core.style import load_stylesheet
from pymxs import runtime as rt
@ -222,7 +222,7 @@ def reset_scene_resolution():
contains any information regarding scene resolution.
"""
folder_entity = get_current_project_folder(
folder_entity = get_current_folder_entity(
fields={"attrib.resolutionWidth", "attrib.resolutionHeight"}
)
folder_attributes = folder_entity["attrib"]
@ -243,7 +243,7 @@ def get_frame_range(folder_entiy=None) -> Union[Dict[str, Any], None]:
"""
# Set frame start/end
if folder_entiy is None:
folder_entiy = get_current_project_folder()
folder_entiy = get_current_folder_entity()
folder_attributes = folder_entiy["attrib"]
frame_start = folder_attributes.get("frameStart")

View file

@ -3,7 +3,7 @@ from pymxs import runtime as rt
from ayon_core.lib import Logger
from ayon_core.settings import get_project_settings
from ayon_core.pipeline import get_current_project_name
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
from ayon_core.hosts.max.api.lib import (
set_render_frame_range,
@ -57,7 +57,7 @@ class RenderSettings(object):
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# hard-coded, should be customized in the setting
folder_attributes = get_current_project_folder()["attrib"]
folder_attributes = get_current_folder_entity()["attrib"]
# get project resolution
width = folder_attributes.get("resolutionWidth")

View file

@ -240,10 +240,10 @@ def get_previous_loaded_object(container: str):
node_list(list): list of nodes which are previously loaded
"""
node_list = []
sel_list = rt.getProperty(container.modifiers[0].openPypeData, "sel_list")
for obj in rt.Objects:
if str(obj) in sel_list:
node_list.append(obj)
node_transform_monitor_list = rt.getProperty(
container.modifiers[0].openPypeData, "all_handles")
for node_transform_monitor in node_transform_monitor_list:
node_list.append(node_transform_monitor.node)
return node_list

View file

@ -2,7 +2,7 @@
"""Pre-launch to force 3ds max startup script."""
import os
from ayon_core.hosts.max import MAX_HOST_DIR
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class ForceStartupScript(PreLaunchHook):

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""Pre-launch hook to inject python environment."""
import os
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class InjectPythonPath(PreLaunchHook):

View file

@ -1,4 +1,4 @@
from ayon_core.lib.applications import PreLaunchHook, LaunchTypes
from ayon_applications import PreLaunchHook, LaunchTypes
class SetPath(PreLaunchHook):

View file

@ -7,7 +7,6 @@ from ayon_core.hosts.max.api.lib import (
maintained_selection,
object_transform_set
)
from ayon_core.hosts.max.api.lib import maintained_selection
from ayon_core.hosts.max.api.pipeline import (
containerise,
get_previous_loaded_object,

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import pyblish.api
from ayon_core.pipeline import PublishValidationError
from pymxs import runtime as rt
class ValidateCameraContent(pyblish.api.InstancePlugin):

View file

@ -140,7 +140,7 @@ class ValidateRenderPasses(OptionalPyblishPluginMixin,
invalid = []
if instance.name not in file_name:
cls.log.error("The renderpass filename should contain the instance name.")
invalid.append((f"Invalid instance name",
invalid.append(("Invalid instance name",
file_name))
if renderpass is not None:
if not file_name.rstrip(".").endswith(renderpass):

View file

@ -4,7 +4,10 @@ from __future__ import absolute_import
import pyblish.api
import ayon_api
from ayon_core.pipeline.publish import get_errored_instances_from_context
from ayon_core.pipeline.publish import (
get_errored_instances_from_context,
get_errored_plugins_from_context
)
class GenerateUUIDsOnInvalidAction(pyblish.api.Action):
@ -112,20 +115,25 @@ class SelectInvalidAction(pyblish.api.Action):
except ImportError:
raise ImportError("Current host is not Maya")
errored_instances = get_errored_instances_from_context(context,
plugin=plugin)
# Get the invalid nodes for the plug-ins
self.log.info("Finding invalid nodes..")
invalid = list()
for instance in errored_instances:
invalid_nodes = plugin.get_invalid(instance)
if invalid_nodes:
if isinstance(invalid_nodes, (list, tuple)):
invalid.extend(invalid_nodes)
else:
self.log.warning("Plug-in returned to be invalid, "
"but has no selectable nodes.")
if issubclass(plugin, pyblish.api.ContextPlugin):
errored_plugins = get_errored_plugins_from_context(context)
if plugin in errored_plugins:
invalid = plugin.get_invalid(context)
else:
errored_instances = get_errored_instances_from_context(
context, plugin=plugin
)
for instance in errored_instances:
invalid_nodes = plugin.get_invalid(instance)
if invalid_nodes:
if isinstance(invalid_nodes, (list, tuple)):
invalid.extend(invalid_nodes)
else:
self.log.warning("Plug-in returned to be invalid, "
"but has no selectable nodes.")
# Ensure unique (process each node only once)
invalid = list(set(invalid))

View file

@ -113,7 +113,9 @@ def override_toolbox_ui():
annotation="Look Manager",
label="Look Manager",
image=os.path.join(icons, "lookmanager.png"),
command=show_look_assigner,
command=lambda: show_look_assigner(
parent=parent_widget
),
width=icon_size,
height=icon_size,
parent=parent

View file

@ -37,7 +37,7 @@ from ayon_core.pipeline import (
AYON_CONTAINER_ID,
)
from ayon_core.lib import NumberDef
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
from ayon_core.pipeline.create import CreateContext
from ayon_core.lib.profiles_filtering import filter_profiles
@ -131,7 +131,7 @@ def get_main_window():
def suspended_refresh(suspend=True):
"""Suspend viewport refreshes
cmds.ogs(pause=True) is a toggle so we cant pass False.
cmds.ogs(pause=True) is a toggle so we can't pass False.
"""
if IS_HEADLESS:
yield
@ -583,7 +583,7 @@ def pairwise(iterable):
def collect_animation_defs(fps=False):
"""Get the basic animation attribute defintions for the publisher.
"""Get the basic animation attribute definitions for the publisher.
Returns:
OrderedDict
@ -1876,18 +1876,9 @@ def list_looks(project_name, folder_id):
list[dict[str, Any]]: List of look products.
"""
# # get all products with look leading in
# the name associated with the asset
# TODO this should probably look for product type 'look' instead of
# checking product name that can not start with product type
product_entities = ayon_api.get_products(
project_name, folder_ids=[folder_id]
)
return [
product_entity
for product_entity in product_entities
if product_entity["name"].startswith("look")
]
return list(ayon_api.get_products(
project_name, folder_ids=[folder_id], product_types={"look"}
))
def assign_look_by_version(nodes, version_id):
@ -1906,12 +1897,15 @@ def assign_look_by_version(nodes, version_id):
project_name = get_current_project_name()
# Get representations of shader file and relationships
look_representation = ayon_api.get_representation_by_name(
project_name, "ma", version_id
)
json_representation = ayon_api.get_representation_by_name(
project_name, "json", version_id
)
representations = list(ayon_api.get_representations(
project_name=project_name,
representation_names={"ma", "json"},
version_ids=[version_id]
))
look_representation = next(
repre for repre in representations if repre["name"] == "ma")
json_representation = next(
repre for repre in representations if repre["name"] == "json")
# See if representation is already loaded, if so reuse it.
host = registered_host()
@ -1948,7 +1942,7 @@ def assign_look_by_version(nodes, version_id):
apply_shaders(relationships, shader_nodes, nodes)
def assign_look(nodes, product_name="lookDefault"):
def assign_look(nodes, product_name="lookMain"):
"""Assigns a look to a node.
Optimizes the nodes by grouping by folder id and finding
@ -1981,14 +1975,10 @@ def assign_look(nodes, product_name="lookDefault"):
product_entity["id"]
for product_entity in product_entities_by_folder_id.values()
}
last_version_entities = ayon_api.get_last_versions(
last_version_entities_by_product_id = ayon_api.get_last_versions(
project_name,
product_ids
)
last_version_entities_by_product_id = {
last_version_entity["productId"]: last_version_entity
for last_version_entity in last_version_entities
}
for folder_id, asset_nodes in grouped.items():
product_entity = product_entities_by_folder_id.get(folder_id)
@ -2125,22 +2115,6 @@ def get_related_sets(node):
"""
# Ignore specific suffices
ignore_suffices = ["out_SET", "controls_SET", "_INST", "_CON"]
# Default nodes to ignore
defaults = {"defaultLightSet", "defaultObjectSet"}
# Ids to ignore
ignored = {
AVALON_INSTANCE_ID,
AVALON_CONTAINER_ID,
AYON_INSTANCE_ID,
AYON_CONTAINER_ID,
}
view_sets = get_isolate_view_sets()
sets = cmds.listSets(object=node, extendToShape=False)
if not sets:
return []
@ -2151,25 +2125,47 @@ def get_related_sets(node):
# returned by `cmds.listSets(allSets=True)`
sets = cmds.ls(sets)
# Ids to ignore
ignored = {
AVALON_INSTANCE_ID,
AVALON_CONTAINER_ID,
AYON_INSTANCE_ID,
AYON_CONTAINER_ID,
}
# Ignore `avalon.container`
sets = [s for s in sets if
not cmds.attributeQuery("id", node=s, exists=True) or
not cmds.getAttr("%s.id" % s) in ignored]
sets = [
s for s in sets
if (
not cmds.attributeQuery("id", node=s, exists=True)
or cmds.getAttr(f"{s}.id") not in ignored
)
]
if not sets:
return sets
# Exclude deformer sets (`type=2` for `maya.cmds.listSets`)
deformer_sets = cmds.listSets(object=node,
extendToShape=False,
type=2) or []
deformer_sets = set(deformer_sets) # optimize lookup
sets = [s for s in sets if s not in deformer_sets]
exclude_sets = cmds.listSets(object=node,
extendToShape=False,
type=2) or []
exclude_sets = set(exclude_sets) # optimize lookup
# Default nodes to ignore
exclude_sets.update({"defaultLightSet", "defaultObjectSet"})
# Filter out the sets to exclude
sets = [s for s in sets if s not in exclude_sets]
# Ignore when the set has a specific suffix
sets = [s for s in sets if not any(s.endswith(x) for x in ignore_suffices)]
ignore_suffices = ("out_SET", "controls_SET", "_INST", "_CON")
sets = [s for s in sets if not s.endswith(ignore_suffices)]
if not sets:
return sets
# Ignore viewport filter view sets (from isolate select and
# viewports)
view_sets = get_isolate_view_sets()
sets = [s for s in sets if s not in view_sets]
sets = [s for s in sets if s not in defaults]
return sets
@ -2440,12 +2436,10 @@ def set_scene_fps(fps, update=True):
cmds.currentUnit(time=unit, updateAnimation=update)
# Set time slider data back to previous state
cmds.playbackOptions(edit=True, minTime=start_frame)
cmds.playbackOptions(edit=True, maxTime=end_frame)
# Set animation data
cmds.playbackOptions(edit=True, animationStartTime=animation_start)
cmds.playbackOptions(edit=True, animationEndTime=animation_end)
cmds.playbackOptions(minTime=start_frame,
maxTime=end_frame,
animationStartTime=animation_start,
animationEndTime=animation_end)
cmds.currentTime(current_frame, edit=True, update=True)
@ -2521,7 +2515,7 @@ def get_fps_for_current_context():
def get_frame_range(include_animation_range=False):
"""Get the current folder frame range and handles.
"""Get the current task frame range and handles.
Args:
include_animation_range (bool, optional): Whether to include
@ -2529,25 +2523,34 @@ def get_frame_range(include_animation_range=False):
range of the timeline. It is excluded by default.
Returns:
dict: Folder's expected frame range values.
dict: Task's expected frame range values.
"""
# Set frame start/end
project_name = get_current_project_name()
folder_path = get_current_folder_path()
folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
folder_attributes = folder_entity["attrib"]
task_name = get_current_task_name()
frame_start = folder_attributes.get("frameStart")
frame_end = folder_attributes.get("frameEnd")
folder_entity = ayon_api.get_folder_by_path(
project_name,
folder_path,
fields={"id"})
task_entity = ayon_api.get_task_by_name(
project_name, folder_entity["id"], task_name
)
task_attributes = task_entity["attrib"]
frame_start = task_attributes.get("frameStart")
frame_end = task_attributes.get("frameEnd")
if frame_start is None or frame_end is None:
cmds.warning("No edit information found for '{}'".format(folder_path))
return
handle_start = folder_attributes.get("handleStart") or 0
handle_end = folder_attributes.get("handleEnd") or 0
handle_start = task_attributes.get("handleStart") or 0
handle_end = task_attributes.get("handleEnd") or 0
frame_range = {
"frameStart": frame_start,
@ -2561,14 +2564,10 @@ def get_frame_range(include_animation_range=False):
# Some usages of this function use the full dictionary to define
# instance attributes for which we want to exclude the animation
# keys. That is why these are excluded by default.
task_name = get_current_task_name()
settings = get_project_settings(project_name)
task_entity = ayon_api.get_task_by_name(
project_name, folder_entity["id"], task_name
)
task_type = None
if task_entity:
task_type = task_entity["taskType"]
task_type = task_entity["taskType"]
include_handles_settings = settings["maya"]["include_handles"]
@ -2637,7 +2636,7 @@ def reset_scene_resolution():
None
"""
folder_attributes = get_current_project_folder()["attrib"]
folder_attributes = get_current_folder_entity()["attrib"]
# Set resolution
width = folder_attributes.get("resolutionWidth", 1920)
@ -2647,31 +2646,114 @@ def reset_scene_resolution():
set_scene_resolution(width, height, pixelAspect)
def set_context_settings():
def set_context_settings(
fps=True,
resolution=True,
frame_range=True,
colorspace=True
):
"""Apply the project settings from the project definition
Settings can be overwritten by an folder if the folder.attrib contains
Settings can be overwritten by an asset if the asset.data contains
any information regarding those settings.
Examples of settings:
fps
resolution
renderer
Args:
fps (bool): Whether to set the scene FPS.
resolution (bool): Whether to set the render resolution.
frame_range (bool): Whether to reset the time slide frame ranges.
colorspace (bool): Whether to reset the colorspace.
Returns:
None
"""
# Set project fps
set_scene_fps(get_fps_for_current_context())
if fps:
# Set project fps
set_scene_fps(get_fps_for_current_context())
reset_scene_resolution()
if resolution:
reset_scene_resolution()
# Set frame range.
reset_frame_range()
if frame_range:
reset_frame_range(fps=False)
# Set colorspace
set_colorspace()
if colorspace:
set_colorspace()
def prompt_reset_context():
"""Prompt the user what context settings to reset.
This prompt is used on saving to a different task to allow the scene to
get matched to the new context.
"""
# TODO: Cleanup this prototyped mess of imports and odd dialog
from ayon_core.tools.attribute_defs.dialog import (
AttributeDefinitionsDialog
)
from ayon_core.style import load_stylesheet
from ayon_core.lib import BoolDef, UILabelDef
definitions = [
UILabelDef(
label=(
"You are saving your workfile into a different folder or task."
"\n\n"
"Would you like to update some settings to the new context?\n"
)
),
BoolDef(
"fps",
label="FPS",
tooltip="Reset workfile FPS",
default=True
),
BoolDef(
"frame_range",
label="Frame Range",
tooltip="Reset workfile start and end frame ranges",
default=True
),
BoolDef(
"resolution",
label="Resolution",
tooltip="Reset workfile resolution",
default=True
),
BoolDef(
"colorspace",
label="Colorspace",
tooltip="Reset workfile resolution",
default=True
),
BoolDef(
"instances",
label="Publish instances",
tooltip="Update all publish instance's folder and task to match "
"the new folder and task",
default=True
),
]
dialog = AttributeDefinitionsDialog(definitions)
dialog.setWindowTitle("Saving to different context.")
dialog.setStyleSheet(load_stylesheet())
if not dialog.exec_():
return None
options = dialog.get_values()
with suspended_refresh():
set_context_settings(
fps=options["fps"],
resolution=options["resolution"],
frame_range=options["frame_range"],
colorspace=options["colorspace"]
)
if options["instances"]:
update_content_on_context_change()
dialog.deleteLater()
# Valid FPS
@ -3163,7 +3245,7 @@ def update_content_on_context_change():
This will update scene content to match new folder on context change
"""
scene_sets = cmds.listSets(allSets=True)
folder_entity = get_current_project_folder()
folder_entity = get_current_folder_entity()
folder_attributes = folder_entity["attrib"]
new_folder_path = folder_entity["path"]
for s in scene_sets:
@ -3834,7 +3916,7 @@ def get_color_management_output_transform():
def image_info(file_path):
# type: (str) -> dict
"""Based on tha texture path, get its bit depth and format information.
"""Based on the texture path, get its bit depth and format information.
Take reference from makeTx.py in Arnold:
ImageInfo(filename): Get Image Information for colorspace
AiTextureGetFormat(filename): Get Texture Format
@ -3922,17 +4004,26 @@ def len_flattened(components):
return n
def get_all_children(nodes):
def get_all_children(nodes, ignore_intermediate_objects=False):
"""Return all children of `nodes` including each instanced child.
Using maya.cmds.listRelatives(allDescendents=True) includes only the first
instance. As such, this function acts as an optimal replacement with a
focus on a fast query.
Args:
nodes (iterable): List of nodes to get children for.
ignore_intermediate_objects (bool): Ignore any children that
are intermediate objects.
Returns:
set: Children of input nodes.
"""
sel = OpenMaya.MSelectionList()
traversed = set()
iterator = OpenMaya.MItDag(OpenMaya.MItDag.kDepthFirst)
fn_dag = OpenMaya.MFnDagNode()
for node in nodes:
if node in traversed:
@ -3949,6 +4040,13 @@ def get_all_children(nodes):
iterator.next() # noqa: B305
while not iterator.isDone():
if ignore_intermediate_objects:
fn_dag.setObject(iterator.currentItem())
if fn_dag.isIntermediateObject:
iterator.prune()
iterator.next() # noqa: B305
continue
path = iterator.fullPathName()
if path in traversed:
@ -3959,7 +4057,7 @@ def get_all_children(nodes):
traversed.add(path)
iterator.next() # noqa: B305
return list(traversed)
return traversed
def get_capture_preset(

View file

@ -7,7 +7,7 @@ from ayon_core.lib import Logger
from ayon_core.settings import get_project_settings
from ayon_core.pipeline import CreatorError, get_current_project_name
from ayon_core.pipeline.context_tools import get_current_project_folder
from ayon_core.pipeline.context_tools import get_current_folder_entity
from ayon_core.hosts.maya.api.lib import reset_frame_range
@ -77,7 +77,7 @@ class RenderSettings(object):
renderer = cmds.getAttr(
'defaultRenderGlobals.currentRenderer').lower()
folder_entity = get_current_project_folder()
folder_entity = get_current_folder_entity()
folder_attributes = folder_entity["attrib"]
# project_settings/maya/create/CreateRender/aov_separator
try:

Some files were not shown because too many files have changed in this diff Show more