Merge remote-tracking branch 'upstream/develop' into fusion_new_publisher

This commit is contained in:
Roy Nieterau 2022-09-26 15:49:59 +02:00
commit 88e0ccfa1c
71 changed files with 3444 additions and 2104 deletions

View file

@ -1,13 +1,21 @@
# Changelog
## [3.14.3-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD)
## [3.14.3-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.14.2...HEAD)
**🚀 Enhancements**
- Maya: better logging in Maketx [\#3886](https://github.com/pypeclub/OpenPype/pull/3886)
- Photoshop: review can be turned off [\#3885](https://github.com/pypeclub/OpenPype/pull/3885)
- TrayPublisher: added persisting of last selected project [\#3871](https://github.com/pypeclub/OpenPype/pull/3871)
- TrayPublisher: added text filter on project name to Tray Publisher [\#3867](https://github.com/pypeclub/OpenPype/pull/3867)
- Github issues adding `running version` section [\#3864](https://github.com/pypeclub/OpenPype/pull/3864)
- Publisher: Increase size of main window [\#3862](https://github.com/pypeclub/OpenPype/pull/3862)
- Flame: make migratable projects after creation [\#3860](https://github.com/pypeclub/OpenPype/pull/3860)
- Photoshop: synchronize image version with workfile [\#3854](https://github.com/pypeclub/OpenPype/pull/3854)
- General: Transcoding handle float2 attr type [\#3849](https://github.com/pypeclub/OpenPype/pull/3849)
- General: Simple script for getting license information about used packages [\#3843](https://github.com/pypeclub/OpenPype/pull/3843)
- Houdini: Increment current file on workfile publish [\#3840](https://github.com/pypeclub/OpenPype/pull/3840)
- Publisher: Add new publisher to host tools [\#3833](https://github.com/pypeclub/OpenPype/pull/3833)
- General: lock task workfiles when they are working on [\#3810](https://github.com/pypeclub/OpenPype/pull/3810)
@ -15,9 +23,15 @@
**🐛 Bug fixes**
- Flame: loading multilayer exr to batch/reel is working [\#3901](https://github.com/pypeclub/OpenPype/pull/3901)
- Hiero: Fix inventory check on launch [\#3895](https://github.com/pypeclub/OpenPype/pull/3895)
- WebPublisher: Fix import after refactor [\#3891](https://github.com/pypeclub/OpenPype/pull/3891)
- TVPaint: Fix renaming of rendered files [\#3882](https://github.com/pypeclub/OpenPype/pull/3882)
- Publisher: Nice checkbox visible in Python 2 [\#3877](https://github.com/pypeclub/OpenPype/pull/3877)
- Settings: Add missing default settings [\#3870](https://github.com/pypeclub/OpenPype/pull/3870)
- General: Copy of workfile does not use 'copy' function but 'copyfile' [\#3869](https://github.com/pypeclub/OpenPype/pull/3869)
- Tray Publisher: skip plugin if otioTimeline is missing [\#3856](https://github.com/pypeclub/OpenPype/pull/3856)
- Flame: retimed attributes are integrated with settings [\#3855](https://github.com/pypeclub/OpenPype/pull/3855)
- Maya: Extract Playblast fix textures + labelize viewport show settings [\#3852](https://github.com/pypeclub/OpenPype/pull/3852)
- Ftrack: Url validation does not require ftrackapp [\#3834](https://github.com/pypeclub/OpenPype/pull/3834)
- Maya+Ftrack: Change typo in family name `mayaascii` -\> `mayaAscii` [\#3820](https://github.com/pypeclub/OpenPype/pull/3820)
@ -25,14 +39,16 @@
**🔀 Refactored code**
- Houdini: Use new Extractor location [\#3894](https://github.com/pypeclub/OpenPype/pull/3894)
- Harmony: Use new Extractor location [\#3893](https://github.com/pypeclub/OpenPype/pull/3893)
- Hiero: Use new Extractor location [\#3851](https://github.com/pypeclub/OpenPype/pull/3851)
- Maya: Remove old legacy \(ftrack\) plug-ins that are of no use anymore [\#3819](https://github.com/pypeclub/OpenPype/pull/3819)
- Nuke: Use new Extractor location [\#3799](https://github.com/pypeclub/OpenPype/pull/3799)
- Maya: Use new Extractor location [\#3775](https://github.com/pypeclub/OpenPype/pull/3775)
- General: Change publish template settings location [\#3755](https://github.com/pypeclub/OpenPype/pull/3755)
**Merged pull requests:**
- Maya: RenderSettings set default image format for V-Ray+Redshift to exr [\#3879](https://github.com/pypeclub/OpenPype/pull/3879)
- Remove lockfile during publish [\#3874](https://github.com/pypeclub/OpenPype/pull/3874)
## [3.14.2](https://github.com/pypeclub/OpenPype/tree/3.14.2) (2022-09-12)
@ -50,8 +66,6 @@
- General: Better pixmap scaling [\#3809](https://github.com/pypeclub/OpenPype/pull/3809)
- Photoshop: attempt to speed up ExtractImage [\#3793](https://github.com/pypeclub/OpenPype/pull/3793)
- SyncServer: Added cli commands for sync server [\#3765](https://github.com/pypeclub/OpenPype/pull/3765)
- Kitsu: Drop 'entities root' setting. [\#3739](https://github.com/pypeclub/OpenPype/pull/3739)
- git: update gitignore [\#3722](https://github.com/pypeclub/OpenPype/pull/3722)
**🐛 Bug fixes**
@ -77,11 +91,9 @@
- General: Move create project folders to pipeline [\#3768](https://github.com/pypeclub/OpenPype/pull/3768)
- General: Create project function moved to client code [\#3766](https://github.com/pypeclub/OpenPype/pull/3766)
- Maya: Refactor submit deadline to use AbstractSubmitDeadline [\#3759](https://github.com/pypeclub/OpenPype/pull/3759)
- General: Change publish template settings location [\#3755](https://github.com/pypeclub/OpenPype/pull/3755)
- General: Move hostdirname functionality into host [\#3749](https://github.com/pypeclub/OpenPype/pull/3749)
- General: Move publish utils to pipeline [\#3745](https://github.com/pypeclub/OpenPype/pull/3745)
- Houdini: Define houdini as addon [\#3735](https://github.com/pypeclub/OpenPype/pull/3735)
- Fusion: Defined fusion as addon [\#3733](https://github.com/pypeclub/OpenPype/pull/3733)
- Resolve: Define resolve as addon [\#3727](https://github.com/pypeclub/OpenPype/pull/3727)
**Merged pull requests:**
@ -95,37 +107,18 @@
**🚀 Enhancements**
- General: Thumbnail can use project roots [\#3750](https://github.com/pypeclub/OpenPype/pull/3750)
- Settings: Remove settings lock on tray exit [\#3720](https://github.com/pypeclub/OpenPype/pull/3720)
**🐛 Bug fixes**
- Maya: Fix typo in getPanel argument `with\_focus` -\> `withFocus` [\#3753](https://github.com/pypeclub/OpenPype/pull/3753)
- General: Smaller fixes of imports [\#3748](https://github.com/pypeclub/OpenPype/pull/3748)
- General: Logger tweaks [\#3741](https://github.com/pypeclub/OpenPype/pull/3741)
- Nuke: missing job dependency if multiple bake streams [\#3737](https://github.com/pypeclub/OpenPype/pull/3737)
- Nuke: color-space settings from anatomy is working [\#3721](https://github.com/pypeclub/OpenPype/pull/3721)
- Settings: Fix studio default anatomy save [\#3716](https://github.com/pypeclub/OpenPype/pull/3716)
**🔀 Refactored code**
- General: Move delivery logic to pipeline [\#3751](https://github.com/pypeclub/OpenPype/pull/3751)
- General: Host addons cleanup [\#3744](https://github.com/pypeclub/OpenPype/pull/3744)
- Webpublisher: Webpublisher is used as addon [\#3740](https://github.com/pypeclub/OpenPype/pull/3740)
- Photoshop: Defined photoshop as addon [\#3736](https://github.com/pypeclub/OpenPype/pull/3736)
- Harmony: Defined harmony as addon [\#3734](https://github.com/pypeclub/OpenPype/pull/3734)
- Flame: Defined flame as addon [\#3732](https://github.com/pypeclub/OpenPype/pull/3732)
- General: Module interfaces cleanup [\#3731](https://github.com/pypeclub/OpenPype/pull/3731)
- AfterEffects: Move AE functions from general lib [\#3730](https://github.com/pypeclub/OpenPype/pull/3730)
- Blender: Define blender as module [\#3729](https://github.com/pypeclub/OpenPype/pull/3729)
- AfterEffects: Define AfterEffects as module [\#3728](https://github.com/pypeclub/OpenPype/pull/3728)
- General: Replace PypeLogger with Logger [\#3725](https://github.com/pypeclub/OpenPype/pull/3725)
- Nuke: Define nuke as module [\#3724](https://github.com/pypeclub/OpenPype/pull/3724)
- General: Move subset name functionality [\#3723](https://github.com/pypeclub/OpenPype/pull/3723)
- General: Move creators plugin getter [\#3714](https://github.com/pypeclub/OpenPype/pull/3714)
**Merged pull requests:**
- Hiero: Define hiero as module [\#3717](https://github.com/pypeclub/OpenPype/pull/3717)
## [3.14.0](https://github.com/pypeclub/OpenPype/tree/3.14.0) (2022-08-18)

View file

@ -0,0 +1,18 @@
Addon distribution tool
------------------------
Code in this folder is backend portion of Addon distribution logic for v4 server.
Each host, module will be separate Addon in the future. Each v4 server could run different set of Addons.
Client (running on artist machine) will in the first step ask v4 for list of enabled addons.
(It expects list of json documents matching to `addon_distribution.py:AddonInfo` object.)
Next it will compare presence of enabled addon version in local folder. In the case of missing version of
an addon, client will use information in the addon to download (from http/shared local disk/git) zip file
and unzip it.
Required part of addon distribution will be sharing of dependencies (python libraries, utilities) which is not part of this folder.
Location of this folder might change in the future as it will be required for a clint to add this folder to sys.path reliably.
This code needs to be independent on Openpype code as much as possible!

View file

@ -0,0 +1,208 @@
import os
from enum import Enum
from abc import abstractmethod
import attr
import logging
import requests
import platform
import shutil
from .file_handler import RemoteFileHandler
from .addon_info import AddonInfo
class UpdateState(Enum):
EXISTS = "exists"
UPDATED = "updated"
FAILED = "failed"
class AddonDownloader:
log = logging.getLogger(__name__)
def __init__(self):
self._downloaders = {}
def register_format(self, downloader_type, downloader):
self._downloaders[downloader_type.value] = downloader
def get_downloader(self, downloader_type):
downloader = self._downloaders.get(downloader_type)
if not downloader:
raise ValueError(f"{downloader_type} not implemented")
return downloader()
@classmethod
@abstractmethod
def download(cls, source, destination):
"""Returns url to downloaded addon zip file.
Args:
source (dict): {type:"http", "url":"https://} ...}
destination (str): local folder to unzip
Returns:
(str) local path to addon zip file
"""
pass
@classmethod
def check_hash(cls, addon_path, addon_hash):
"""Compares 'hash' of downloaded 'addon_url' file.
Args:
addon_path (str): local path to addon zip file
addon_hash (str): sha256 hash of zip file
Raises:
ValueError if hashes doesn't match
"""
if not os.path.exists(addon_path):
raise ValueError(f"{addon_path} doesn't exist.")
if not RemoteFileHandler.check_integrity(addon_path,
addon_hash,
hash_type="sha256"):
raise ValueError(f"{addon_path} doesn't match expected hash.")
@classmethod
def unzip(cls, addon_zip_path, destination):
"""Unzips local 'addon_zip_path' to 'destination'.
Args:
addon_zip_path (str): local path to addon zip file
destination (str): local folder to unzip
"""
RemoteFileHandler.unzip(addon_zip_path, destination)
os.remove(addon_zip_path)
@classmethod
def remove(cls, addon_url):
pass
class OSAddonDownloader(AddonDownloader):
@classmethod
def download(cls, source, destination):
# OS doesnt need to download, unzip directly
addon_url = source["path"].get(platform.system().lower())
if not os.path.exists(addon_url):
raise ValueError("{} is not accessible".format(addon_url))
return addon_url
class HTTPAddonDownloader(AddonDownloader):
CHUNK_SIZE = 100000
@classmethod
def download(cls, source, destination):
source_url = source["url"]
cls.log.debug(f"Downloading {source_url} to {destination}")
file_name = os.path.basename(destination)
_, ext = os.path.splitext(file_name)
if (ext.replace(".", '') not
in set(RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS)):
file_name += ".zip"
RemoteFileHandler.download_url(source_url,
destination,
filename=file_name)
return os.path.join(destination, file_name)
def get_addons_info(server_endpoint):
"""Returns list of addon information from Server"""
# TODO temp
# addon_info = AddonInfo(
# **{"name": "openpype_slack",
# "version": "1.0.0",
# "addon_url": "c:/projects/openpype_slack_1.0.0.zip",
# "type": UrlType.FILESYSTEM,
# "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658"}) # noqa
#
# http_addon = AddonInfo(
# **{"name": "openpype_slack",
# "version": "1.0.0",
# "addon_url": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing", # noqa
# "type": UrlType.HTTP,
# "hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658"}) # noqa
response = requests.get(server_endpoint)
if not response.ok:
raise Exception(response.text)
addons_info = []
for addon in response.json():
addons_info.append(AddonInfo(**addon))
return addons_info
def update_addon_state(addon_infos, destination_folder, factory,
log=None):
"""Loops through all 'addon_infos', compares local version, unzips.
Loops through server provided list of dictionaries with information about
available addons. Looks if each addon is already present and deployed.
If isn't, addon zip gets downloaded and unzipped into 'destination_folder'.
Args:
addon_infos (list of AddonInfo)
destination_folder (str): local path
factory (AddonDownloader): factory to get appropriate downloader per
addon type
log (logging.Logger)
Returns:
(dict): {"addon_full_name": UpdateState.value
(eg. "exists"|"updated"|"failed")
"""
if not log:
log = logging.getLogger(__name__)
download_states = {}
for addon in addon_infos:
full_name = "{}_{}".format(addon.name, addon.version)
addon_dest = os.path.join(destination_folder, full_name)
if os.path.isdir(addon_dest):
log.debug(f"Addon version folder {addon_dest} already exists.")
download_states[full_name] = UpdateState.EXISTS.value
continue
for source in addon.sources:
download_states[full_name] = UpdateState.FAILED.value
try:
downloader = factory.get_downloader(source.type)
zip_file_path = downloader.download(attr.asdict(source),
addon_dest)
downloader.check_hash(zip_file_path, addon.hash)
downloader.unzip(zip_file_path, addon_dest)
download_states[full_name] = UpdateState.UPDATED.value
break
except Exception:
log.warning(f"Error happened during updating {addon.name}",
exc_info=True)
if os.path.isdir(addon_dest):
log.debug(f"Cleaning {addon_dest}")
shutil.rmtree(addon_dest)
return download_states
def check_addons(server_endpoint, addon_folder, downloaders):
"""Main entry point to compare existing addons with those on server.
Args:
server_endpoint (str): url to v4 server endpoint
addon_folder (str): local dir path for addons
downloaders (AddonDownloader): factory of downloaders
Raises:
(RuntimeError) if any addon failed update
"""
addons_info = get_addons_info(server_endpoint)
result = update_addon_state(addons_info,
addon_folder,
downloaders)
if UpdateState.FAILED.value in result.values():
raise RuntimeError(f"Unable to update some addons {result}")
def cli(*args):
raise NotImplementedError

View file

@ -0,0 +1,80 @@
import attr
from enum import Enum
class UrlType(Enum):
HTTP = "http"
GIT = "git"
FILESYSTEM = "filesystem"
@attr.s
class MultiPlatformPath(object):
windows = attr.ib(default=None)
linux = attr.ib(default=None)
darwin = attr.ib(default=None)
@attr.s
class AddonSource(object):
type = attr.ib()
@attr.s
class LocalAddonSource(AddonSource):
path = attr.ib(default=attr.Factory(MultiPlatformPath))
@attr.s
class WebAddonSource(AddonSource):
url = attr.ib(default=None)
@attr.s
class VersionData(object):
version_data = attr.ib(default=None)
@attr.s
class AddonInfo(object):
"""Object matching json payload from Server"""
name = attr.ib()
version = attr.ib()
title = attr.ib(default=None)
sources = attr.ib(default=attr.Factory(dict))
hash = attr.ib(default=None)
description = attr.ib(default=None)
license = attr.ib(default=None)
authors = attr.ib(default=None)
@classmethod
def from_dict(cls, data):
sources = []
production_version = data.get("productionVersion")
if not production_version:
return
# server payload contains info about all versions
# active addon must have 'productionVersion' and matching version info
version_data = data.get("versions", {})[production_version]
for source in version_data.get("clientSourceInfo", []):
if source.get("type") == UrlType.FILESYSTEM.value:
source_addon = LocalAddonSource(type=source["type"],
path=source["path"])
if source.get("type") == UrlType.HTTP.value:
source_addon = WebAddonSource(type=source["type"],
url=source["url"])
sources.append(source_addon)
return cls(name=data.get("name"),
version=production_version,
sources=sources,
hash=data.get("hash"),
description=data.get("description"),
title=data.get("title"),
license=data.get("license"),
authors=data.get("authors"))

View file

@ -21,7 +21,7 @@ class RemoteFileHandler:
'tar.gz', 'tar.xz', 'tar.bz2']
@staticmethod
def calculate_md5(fpath, chunk_size):
def calculate_md5(fpath, chunk_size=10000):
md5 = hashlib.md5()
with open(fpath, 'rb') as f:
for chunk in iter(lambda: f.read(chunk_size), b''):
@ -33,17 +33,45 @@ class RemoteFileHandler:
return md5 == RemoteFileHandler.calculate_md5(fpath, **kwargs)
@staticmethod
def check_integrity(fpath, md5=None):
def calculate_sha256(fpath):
"""Calculate sha256 for content of the file.
Args:
fpath (str): Path to file.
Returns:
str: hex encoded sha256
"""
h = hashlib.sha256()
b = bytearray(128 * 1024)
mv = memoryview(b)
with open(fpath, 'rb', buffering=0) as f:
for n in iter(lambda: f.readinto(mv), 0):
h.update(mv[:n])
return h.hexdigest()
@staticmethod
def check_sha256(fpath, sha256, **kwargs):
return sha256 == RemoteFileHandler.calculate_sha256(fpath, **kwargs)
@staticmethod
def check_integrity(fpath, hash_value=None, hash_type=None):
if not os.path.isfile(fpath):
return False
if md5 is None:
if hash_value is None:
return True
return RemoteFileHandler.check_md5(fpath, md5)
if not hash_type:
raise ValueError("Provide hash type, md5 or sha256")
if hash_type == 'md5':
return RemoteFileHandler.check_md5(fpath, hash_value)
if hash_type == "sha256":
return RemoteFileHandler.check_sha256(fpath, hash_value)
@staticmethod
def download_url(
url, root, filename=None,
md5=None, max_redirect_hops=3
sha256=None, max_redirect_hops=3
):
"""Download a file from a url and place it in root.
Args:
@ -51,7 +79,7 @@ class RemoteFileHandler:
root (str): Directory to place downloaded file in
filename (str, optional): Name to save the file under.
If None, use the basename of the URL
md5 (str, optional): MD5 checksum of the download.
sha256 (str, optional): sha256 checksum of the download.
If None, do not check
max_redirect_hops (int, optional): Maximum number of redirect
hops allowed
@ -64,7 +92,8 @@ class RemoteFileHandler:
os.makedirs(root, exist_ok=True)
# check if file is already present locally
if RemoteFileHandler.check_integrity(fpath, md5):
if RemoteFileHandler.check_integrity(fpath,
sha256, hash_type="sha256"):
print('Using downloaded and verified file: ' + fpath)
return
@ -76,7 +105,7 @@ class RemoteFileHandler:
file_id = RemoteFileHandler._get_google_drive_file_id(url)
if file_id is not None:
return RemoteFileHandler.download_file_from_google_drive(
file_id, root, filename, md5)
file_id, root, filename, sha256)
# download the file
try:
@ -92,20 +121,21 @@ class RemoteFileHandler:
raise e
# check integrity of downloaded file
if not RemoteFileHandler.check_integrity(fpath, md5):
if not RemoteFileHandler.check_integrity(fpath,
sha256, hash_type="sha256"):
raise RuntimeError("File not found or corrupted.")
@staticmethod
def download_file_from_google_drive(file_id, root,
filename=None,
md5=None):
sha256=None):
"""Download a Google Drive file from and place it in root.
Args:
file_id (str): id of file to be downloaded
root (str): Directory to place downloaded file in
filename (str, optional): Name to save the file under.
If None, use the id of the file.
md5 (str, optional): MD5 checksum of the download.
sha256 (str, optional): sha256 checksum of the download.
If None, do not check
"""
# Based on https://stackoverflow.com/questions/38511444/python-download-files-from-google-drive-using-url # noqa
@ -119,8 +149,8 @@ class RemoteFileHandler:
os.makedirs(root, exist_ok=True)
if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(fpath,
md5):
if os.path.isfile(fpath) and RemoteFileHandler.check_integrity(
fpath, sha256, hash_type="sha256"):
print('Using downloaded and verified file: ' + fpath)
else:
session = requests.Session()

View file

@ -0,0 +1,167 @@
import pytest
import attr
import tempfile
from common.openpype_common.distribution.addon_distribution import (
AddonDownloader,
OSAddonDownloader,
HTTPAddonDownloader,
AddonInfo,
update_addon_state,
UpdateState
)
from common.openpype_common.distribution.addon_info import UrlType
@pytest.fixture
def addon_downloader():
addon_downloader = AddonDownloader()
addon_downloader.register_format(UrlType.FILESYSTEM, OSAddonDownloader)
addon_downloader.register_format(UrlType.HTTP, HTTPAddonDownloader)
yield addon_downloader
@pytest.fixture
def http_downloader(addon_downloader):
yield addon_downloader.get_downloader(UrlType.HTTP.value)
@pytest.fixture
def temp_folder():
yield tempfile.mkdtemp()
@pytest.fixture
def sample_addon_info():
addon_info = {
"versions": {
"1.0.0": {
"clientPyproject": {
"tool": {
"poetry": {
"dependencies": {
"nxtools": "^1.6",
"orjson": "^3.6.7",
"typer": "^0.4.1",
"email-validator": "^1.1.3",
"python": "^3.10",
"fastapi": "^0.73.0"
}
}
}
},
"hasSettings": True,
"clientSourceInfo": [
{
"type": "http",
"url": "https://drive.google.com/file/d/1TcuV8c2OV8CcbPeWi7lxOdqWsEqQNPYy/view?usp=sharing" # noqa
},
{
"type": "filesystem",
"path": {
"windows": ["P:/sources/some_file.zip",
"W:/sources/some_file.zip"], # noqa
"linux": ["/mnt/srv/sources/some_file.zip"],
"darwin": ["/Volumes/srv/sources/some_file.zip"]
}
}
],
"frontendScopes": {
"project": {
"sidebar": "hierarchy"
}
}
}
},
"description": "",
"title": "Slack addon",
"name": "openpype_slack",
"productionVersion": "1.0.0",
"hash": "4be25eb6215e91e5894d3c5475aeb1e379d081d3f5b43b4ee15b0891cf5f5658" # noqa
}
yield addon_info
def test_register(printer):
addon_downloader = AddonDownloader()
assert len(addon_downloader._downloaders) == 0, "Contains registered"
addon_downloader.register_format(UrlType.FILESYSTEM, OSAddonDownloader)
assert len(addon_downloader._downloaders) == 1, "Should contain one"
def test_get_downloader(printer, addon_downloader):
assert addon_downloader.get_downloader(UrlType.FILESYSTEM.value), "Should find" # noqa
with pytest.raises(ValueError):
addon_downloader.get_downloader("unknown"), "Shouldn't find"
def test_addon_info(printer, sample_addon_info):
"""Tests parsing of expected payload from v4 server into AadonInfo."""
valid_minimum = {
"name": "openpype_slack",
"productionVersion": "1.0.0",
"versions": {
"1.0.0": {
"clientSourceInfo": [
{
"type": "filesystem",
"path": {
"windows": [
"P:/sources/some_file.zip",
"W:/sources/some_file.zip"],
"linux": [
"/mnt/srv/sources/some_file.zip"],
"darwin": [
"/Volumes/srv/sources/some_file.zip"] # noqa
}
}
]
}
}
}
assert AddonInfo.from_dict(valid_minimum), "Missing required fields"
valid_minimum["versions"].pop("1.0.0")
with pytest.raises(KeyError):
assert not AddonInfo.from_dict(valid_minimum), "Must fail without version data" # noqa
valid_minimum.pop("productionVersion")
assert not AddonInfo.from_dict(
valid_minimum), "none if not productionVersion" # noqa
addon = AddonInfo.from_dict(sample_addon_info)
assert addon, "Should be created"
assert addon.name == "openpype_slack", "Incorrect name"
assert addon.version == "1.0.0", "Incorrect version"
with pytest.raises(TypeError):
assert addon["name"], "Dict approach not implemented"
addon_as_dict = attr.asdict(addon)
assert addon_as_dict["name"], "Dict approach should work"
def test_update_addon_state(printer, sample_addon_info,
temp_folder, addon_downloader):
"""Tests possible cases of addon update."""
addon_info = AddonInfo.from_dict(sample_addon_info)
orig_hash = addon_info.hash
addon_info.hash = "brokenhash"
result = update_addon_state([addon_info], temp_folder, addon_downloader)
assert result["openpype_slack_1.0.0"] == UpdateState.FAILED.value, \
"Update should failed because of wrong hash"
addon_info.hash = orig_hash
result = update_addon_state([addon_info], temp_folder, addon_downloader)
assert result["openpype_slack_1.0.0"] == UpdateState.UPDATED.value, \
"Addon should have been updated"
result = update_addon_state([addon_info], temp_folder, addon_downloader)
assert result["openpype_slack_1.0.0"] == UpdateState.EXISTS.value, \
"Addon should already exist"

View file

@ -5,6 +5,7 @@ from .host import (
from .interfaces import (
IWorkfileHost,
ILoadHost,
IPublishHost,
INewPublisher,
)
@ -16,6 +17,7 @@ __all__ = (
"IWorkfileHost",
"ILoadHost",
"IPublishHost",
"INewPublisher",
"HostDirmap",

View file

@ -282,7 +282,7 @@ class IWorkfileHost:
return self.workfile_has_unsaved_changes()
class INewPublisher:
class IPublishHost:
"""Functions related to new creation system in new publisher.
New publisher is not storing information only about each created instance
@ -306,7 +306,7 @@ class INewPublisher:
workflow.
"""
if isinstance(host, INewPublisher):
if isinstance(host, IPublishHost):
return []
required = [
@ -330,7 +330,7 @@ class INewPublisher:
MissingMethodsError: If there are missing methods on host
implementation.
"""
missing = INewPublisher.get_missing_publish_methods(host)
missing = IPublishHost.get_missing_publish_methods(host)
if missing:
raise MissingMethodsError(host, missing)
@ -368,3 +368,17 @@ class INewPublisher:
"""
pass
class INewPublisher(IPublishHost):
"""Legacy interface replaced by 'IPublishHost'.
Deprecated:
'INewPublisher' is replaced by 'IPublishHost' please change your
imports.
There is no "reasonable" way hot mark these classes as deprecated
to show warning of wrong import. Deprecated since 3.14.* will be
removed in 3.15.*
"""
pass

View file

@ -766,11 +766,11 @@ class MediaInfoFile(object):
_drop_mode = None
_file_pattern = None
def __init__(self, path, **kwargs):
def __init__(self, path, logger=None):
# replace log if any
if kwargs.get("logger"):
self.log = kwargs["logger"]
if logger:
self.log = logger
# test if `dl_get_media_info` paht exists
self._validate_media_script_path()

View file

@ -90,8 +90,7 @@ def containerise(flame_clip_segment,
def ls():
"""List available containers.
"""
# TODO: ls
pass
return []
def parse_container(tl_segment, validate=True):
@ -107,6 +106,7 @@ def update_container(tl_segment, data=None):
# TODO: update_container
pass
def on_pyblish_instance_toggled(instance, old_value, new_value):
"""Toggle node passthrough states on instance toggles."""

View file

@ -678,6 +678,7 @@ class ClipLoader(LoaderPlugin):
`update` logic.
"""
log = log
options = [
qargparse.Boolean(
@ -694,16 +695,20 @@ class OpenClipSolver(flib.MediaInfoFile):
log = log
def __init__(self, openclip_file_path, feed_data):
def __init__(self, openclip_file_path, feed_data, logger=None):
self.out_file = openclip_file_path
# replace log if any
if logger:
self.log = logger
# new feed variables:
feed_path = feed_data.pop("path")
# initialize parent class
super(OpenClipSolver, self).__init__(
feed_path,
**feed_data
logger=logger
)
# get other metadata
@ -751,17 +756,18 @@ class OpenClipSolver(flib.MediaInfoFile):
self.log.info("Building new openClip")
self.log.debug(">> self.clip_data: {}".format(self.clip_data))
# clip data comming from MediaInfoFile
tmp_xml_feeds = self.clip_data.find('tracks/track/feeds')
tmp_xml_feeds.set('currentVersion', self.feed_version_name)
for tmp_feed in tmp_xml_feeds:
tmp_feed.set('vuid', self.feed_version_name)
for tmp_xml_track in self.clip_data.iter("track"):
tmp_xml_feeds = tmp_xml_track.find('feeds')
tmp_xml_feeds.set('currentVersion', self.feed_version_name)
# add colorspace if any is set
if self.feed_colorspace:
self._add_colorspace(tmp_feed, self.feed_colorspace)
for tmp_feed in tmp_xml_track.iter("feed"):
tmp_feed.set('vuid', self.feed_version_name)
self._clear_handler(tmp_feed)
# add colorspace if any is set
if self.feed_colorspace:
self._add_colorspace(tmp_feed, self.feed_colorspace)
self._clear_handler(tmp_feed)
tmp_xml_versions_obj = self.clip_data.find('versions')
tmp_xml_versions_obj.set('currentVersion', self.feed_version_name)
@ -774,6 +780,17 @@ class OpenClipSolver(flib.MediaInfoFile):
self.write_clip_data_to_file(self.out_file, self.clip_data)
def _get_xml_track_obj_by_uid(self, xml_data, uid):
# loop all tracks of input xml data
for xml_track in xml_data.iter("track"):
track_uid = xml_track.get("uid")
self.log.debug(
">> track_uid:uid: {}:{}".format(track_uid, uid))
# get matching uids
if uid == track_uid:
return xml_track
def _update_open_clip(self):
self.log.info("Updating openClip ..")
@ -783,52 +800,81 @@ class OpenClipSolver(flib.MediaInfoFile):
self.log.debug(">> out_xml: {}".format(out_xml))
self.log.debug(">> self.clip_data: {}".format(self.clip_data))
# Get new feed from tmp file
tmp_xml_feed = self.clip_data.find('tracks/track/feeds/feed')
# loop tmp tracks
updated_any = False
for tmp_xml_track in self.clip_data.iter("track"):
# get tmp track uid
tmp_track_uid = tmp_xml_track.get("uid")
self.log.debug(">> tmp_track_uid: {}".format(tmp_track_uid))
self._clear_handler(tmp_xml_feed)
# get out data track by uid
out_track_element = self._get_xml_track_obj_by_uid(
out_xml, tmp_track_uid)
self.log.debug(
">> out_track_element: {}".format(out_track_element))
# update fps from MediaInfoFile class
if self.fps:
tmp_feed_fps_obj = tmp_xml_feed.find(
"startTimecode/rate")
tmp_feed_fps_obj.text = str(self.fps)
# loop tmp feeds
for tmp_xml_feed in tmp_xml_track.iter("feed"):
new_path_obj = tmp_xml_feed.find(
"spans/span/path")
new_path = new_path_obj.text
# update start_frame from MediaInfoFile class
if self.start_frame:
tmp_feed_nb_ticks_obj = tmp_xml_feed.find(
"startTimecode/nbTicks")
tmp_feed_nb_ticks_obj.text = str(self.start_frame)
# check if feed path already exists in track's feeds
if (
out_track_element is not None
and self._feed_exists(out_track_element, new_path)
):
continue
# update drop_mode from MediaInfoFile class
if self.drop_mode:
tmp_feed_drop_mode_obj = tmp_xml_feed.find(
"startTimecode/dropMode")
tmp_feed_drop_mode_obj.text = str(self.drop_mode)
# rename versions on feeds
tmp_xml_feed.set('vuid', self.feed_version_name)
self._clear_handler(tmp_xml_feed)
new_path_obj = tmp_xml_feed.find(
"spans/span/path")
new_path = new_path_obj.text
# update fps from MediaInfoFile class
if self.fps is not None:
tmp_feed_fps_obj = tmp_xml_feed.find(
"startTimecode/rate")
tmp_feed_fps_obj.text = str(self.fps)
feed_added = False
if not self._feed_exists(out_xml, new_path):
tmp_xml_feed.set('vuid', self.feed_version_name)
# Append new temp file feed to .clip source out xml
out_track = out_xml.find("tracks/track")
# add colorspace if any is set
if self.feed_colorspace:
self._add_colorspace(tmp_xml_feed, self.feed_colorspace)
# update start_frame from MediaInfoFile class
if self.start_frame is not None:
tmp_feed_nb_ticks_obj = tmp_xml_feed.find(
"startTimecode/nbTicks")
tmp_feed_nb_ticks_obj.text = str(self.start_frame)
out_feeds = out_track.find('feeds')
out_feeds.set('currentVersion', self.feed_version_name)
out_feeds.append(tmp_xml_feed)
# update drop_mode from MediaInfoFile class
if self.drop_mode is not None:
tmp_feed_drop_mode_obj = tmp_xml_feed.find(
"startTimecode/dropMode")
tmp_feed_drop_mode_obj.text = str(self.drop_mode)
self.log.info(
"Appending new feed: {}".format(
self.feed_version_name))
feed_added = True
# add colorspace if any is set
if self.feed_colorspace is not None:
self._add_colorspace(tmp_xml_feed, self.feed_colorspace)
if feed_added:
# then append/update feed to correct track in output
if out_track_element:
self.log.debug("updating track element ..")
# update already present track
out_feeds = out_track_element.find('feeds')
out_feeds.set('currentVersion', self.feed_version_name)
out_feeds.append(tmp_xml_feed)
self.log.info(
"Appending new feed: {}".format(
self.feed_version_name))
else:
self.log.debug("adding new track element ..")
# create new track as it doesnt exists 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)
out_tracks = out_xml.find("tracks")
out_tracks.append(tmp_xml_track)
updated_any = True
if updated_any:
# Append vUID to versions
out_xml_versions_obj = out_xml.find('versions')
out_xml_versions_obj.set(

View file

@ -22,6 +22,7 @@ class FlamePrelaunch(PreLaunchHook):
in environment var FLAME_SCRIPT_DIR.
"""
app_groups = ["flame"]
permissions = 0o777
wtc_script_path = os.path.join(
opflame.HOST_DIR, "api", "scripts", "wiretap_com.py")
@ -38,6 +39,7 @@ class FlamePrelaunch(PreLaunchHook):
"""Hook entry method."""
project_doc = self.data["project_doc"]
project_name = project_doc["name"]
volume_name = _env.get("FLAME_WIRETAP_VOLUME")
# get image io
project_anatomy = self.data["anatomy"]
@ -81,7 +83,7 @@ class FlamePrelaunch(PreLaunchHook):
data_to_script = {
# from settings
"host_name": _env.get("FLAME_WIRETAP_HOSTNAME") or hostname,
"volume_name": _env.get("FLAME_WIRETAP_VOLUME"),
"volume_name": volume_name,
"group_name": _env.get("FLAME_WIRETAP_GROUP"),
"color_policy": str(imageio_flame["project"]["colourPolicy"]),
@ -99,8 +101,41 @@ class FlamePrelaunch(PreLaunchHook):
app_arguments = self._get_launch_arguments(data_to_script)
# fix project data permission issue
self._fix_permissions(project_name, volume_name)
self.launch_context.launch_args.extend(app_arguments)
def _fix_permissions(self, project_name, volume_name):
"""Work around for project data permissions
Reported issue: when project is created locally on one machine,
it is impossible to migrate it to other machine. Autodesk Flame
is crating some unmanagable files which needs to be opened to 0o777.
Args:
project_name (str): project name
volume_name (str): studio volume
"""
dirs_to_modify = [
"/usr/discreet/project/{}".format(project_name),
"/opt/Autodesk/clip/{}/{}.prj".format(volume_name, project_name),
"/usr/discreet/clip/{}/{}.prj".format(volume_name, project_name)
]
for dirtm in dirs_to_modify:
for root, dirs, files in os.walk(dirtm):
try:
for name in set(dirs) | set(files):
path = os.path.join(root, name)
st = os.stat(path)
if oct(st.st_mode) != self.permissions:
os.chmod(path, self.permissions)
except OSError as exc:
self.log.warning("Not able to open files: {}".format(exc))
def _get_flame_fps(self, fps_num):
fps_table = {
float(23.976): "23.976 fps",

View file

@ -23,10 +23,11 @@ class CreateShotClip(opfapi.Creator):
# nested dictionary (only one level allowed
# for sections and dict)
for _k, _v in v["value"].items():
if presets.get(_k):
if presets.get(_k) is not None:
gui_inputs[k][
"value"][_k]["value"] = presets[_k]
if presets.get(k):
if presets.get(k) is not None:
gui_inputs[k]["value"] = presets[k]
# open widget for plugins inputs

View file

@ -4,6 +4,7 @@ from pprint import pformat
import openpype.hosts.flame.api as opfapi
from openpype.lib import StringTemplate
class LoadClip(opfapi.ClipLoader):
"""Load a subset to timeline as clip
@ -60,8 +61,6 @@ class LoadClip(opfapi.ClipLoader):
"path": self.fname.replace("\\", "/"),
"colorspace": colorspace,
"version": "v{:0>3}".format(version_name),
"logger": self.log
}
self.log.debug(pformat(
loading_context
@ -69,7 +68,8 @@ class LoadClip(opfapi.ClipLoader):
self.log.debug(openclip_path)
# make openpype clip file
opfapi.OpenClipSolver(openclip_path, loading_context).make()
opfapi.OpenClipSolver(
openclip_path, loading_context, logger=self.log).make()
# prepare Reel group in actual desktop
opc = self._get_clip(

View file

@ -64,8 +64,6 @@ class LoadClipBatch(opfapi.ClipLoader):
"path": self.fname.replace("\\", "/"),
"colorspace": colorspace,
"version": "v{:0>3}".format(version_name),
"logger": self.log
}
self.log.debug(pformat(
loading_context
@ -73,7 +71,8 @@ class LoadClipBatch(opfapi.ClipLoader):
self.log.debug(openclip_path)
# make openpype clip file
opfapi.OpenClipSolver(openclip_path, loading_context).make()
opfapi.OpenClipSolver(
openclip_path, loading_context, logger=self.log).make()
# prepare Reel group in actual desktop
opc = self._get_clip(

View file

@ -131,9 +131,8 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
"fps": self.fps,
"workfileFrameStart": workfile_start,
"sourceFirstFrame": int(first_frame),
"notRetimedHandles": (
not marker_data.get("retimedHandles")),
"notRetimedFramerange": (
"retimedHandles": marker_data.get("retimedHandles"),
"shotDurationFromSource": (
not marker_data.get("retimedFramerange")),
"path": file_path,
"flameAddTasks": self.add_tasks,

View file

@ -1,7 +1,6 @@
import os
import re
import tempfile
from pprint import pformat
from copy import deepcopy
import pyblish.api
@ -80,10 +79,10 @@ class ExtractSubsetResources(openpype.api.Extractor):
retimed_data = self._get_retimed_attributes(instance)
# get individual keys
r_handle_start = retimed_data["handle_start"]
r_handle_end = retimed_data["handle_end"]
r_source_dur = retimed_data["source_duration"]
r_speed = retimed_data["speed"]
retimed_handle_start = retimed_data["handle_start"]
retimed_handle_end = retimed_data["handle_end"]
retimed_source_duration = retimed_data["source_duration"]
retimed_speed = retimed_data["speed"]
# get handles value - take only the max from both
handle_start = instance.data["handleStart"]
@ -97,22 +96,23 @@ class ExtractSubsetResources(openpype.api.Extractor):
source_end_handles = instance.data["sourceEndH"]
# retime if needed
if r_speed != 1.0:
if retimed_speed != 1.0:
if retimed_handles:
# handles are retimed
source_start_handles = (
instance.data["sourceStart"] - r_handle_start)
instance.data["sourceStart"] - retimed_handle_start)
source_end_handles = (
source_start_handles
+ (r_source_dur - 1)
+ r_handle_start
+ r_handle_end
+ (retimed_source_duration - 1)
+ retimed_handle_start
+ retimed_handle_end
)
else:
# handles are not retimed
source_end_handles = (
source_start_handles
+ (r_source_dur - 1)
+ (retimed_source_duration - 1)
+ handle_start
+ handle_end
)
@ -121,11 +121,11 @@ class ExtractSubsetResources(openpype.api.Extractor):
frame_start_handle = frame_start - handle_start
repre_frame_start = frame_start_handle
if include_handles:
if r_speed == 1.0 or not retimed_handles:
if retimed_speed == 1.0 or not retimed_handles:
frame_start_handle = frame_start
else:
frame_start_handle = (
frame_start - handle_start) + r_handle_start
frame_start - handle_start) + retimed_handle_start
self.log.debug("_ frame_start_handle: {}".format(
frame_start_handle))
@ -136,6 +136,9 @@ class ExtractSubsetResources(openpype.api.Extractor):
source_duration_handles = (
source_end_handles - source_start_handles) + 1
self.log.debug("_ source_duration_handles: {}".format(
source_duration_handles))
# create staging dir path
staging_dir = self.staging_dir(instance)
@ -159,15 +162,30 @@ class ExtractSubsetResources(openpype.api.Extractor):
if version_data:
instance.data["versionData"].update(version_data)
if r_speed != 1.0:
instance.data["versionData"].update({
"frameStart": frame_start_handle,
"frameEnd": (
(frame_start_handle + source_duration_handles - 1)
- (r_handle_start + r_handle_end)
)
})
self.log.debug("_ i_version_data: {}".format(
# version data start frame
version_frame_start = frame_start
if include_handles:
version_frame_start = frame_start_handle
if retimed_speed != 1.0:
if retimed_handles:
instance.data["versionData"].update({
"frameStart": version_frame_start,
"frameEnd": (
(version_frame_start + source_duration_handles - 1)
- (retimed_handle_start + retimed_handle_end)
)
})
else:
instance.data["versionData"].update({
"handleStart": handle_start,
"handleEnd": handle_end,
"frameStart": version_frame_start,
"frameEnd": (
(version_frame_start + source_duration_handles - 1)
- (handle_start + handle_end)
)
})
self.log.debug("_ version_data: {}".format(
instance.data["versionData"]
))

View file

@ -6,10 +6,10 @@ import csv
from PIL import Image, ImageDraw, ImageFont
import openpype.hosts.harmony.api as harmony
import openpype.api
from openpype.pipeline import publish
class ExtractPalette(openpype.api.Extractor):
class ExtractPalette(publish.Extractor):
"""Extract palette."""
label = "Extract Palette"

View file

@ -3,12 +3,11 @@
import os
import shutil
import openpype.api
from openpype.pipeline import publish
import openpype.hosts.harmony.api as harmony
import openpype.hosts.harmony
class ExtractTemplate(openpype.api.Extractor):
class ExtractTemplate(publish.Extractor):
"""Extract the connected nodes to the composite instance."""
label = "Extract Template"
@ -50,7 +49,7 @@ class ExtractTemplate(openpype.api.Extractor):
dependencies.remove(instance.data["setMembers"][0])
# Export template.
openpype.hosts.harmony.api.export_template(
harmony.export_template(
unique_backdrops, dependencies, filepath
)

View file

@ -4,10 +4,10 @@ import os
import shutil
from zipfile import ZipFile
import openpype.api
from openpype.pipeline import publish
class ExtractWorkfile(openpype.api.Extractor):
class ExtractWorkfile(publish.Extractor):
"""Extract and zip complete workfile folder into zip."""
label = "Extract Workfile"

View file

@ -13,14 +13,10 @@ import hiero
from Qt import QtWidgets
from openpype.client import (
get_project,
get_versions,
get_last_versions,
get_representations,
)
from openpype.client import get_project
from openpype.settings import get_anatomy_settings
from openpype.pipeline import legacy_io, Anatomy
from openpype.pipeline.load import filter_containers
from openpype.lib import Logger
from . import tags
@ -1055,6 +1051,10 @@ def sync_clip_name_to_data_asset(track_items_list):
print("asset was changed in clip: {}".format(ti_name))
def set_track_color(track_item, color):
track_item.source().binItem().setColor(color)
def check_inventory_versions(track_items=None):
"""
Actual version color idetifier of Loaded containers
@ -1066,68 +1066,29 @@ def check_inventory_versions(track_items=None):
"""
from . import parse_container
track_item = track_items or get_track_items()
track_items = track_items or get_track_items()
# presets
clip_color_last = "green"
clip_color = "red"
item_with_repre_id = []
repre_ids = set()
containers = []
# Find all containers and collect it's node and representation ids
for track_item in track_item:
for track_item in track_items:
container = parse_container(track_item)
if container:
repre_id = container["representation"]
repre_ids.add(repre_id)
item_with_repre_id.append((track_item, repre_id))
containers.append(container)
# Skip if nothing was found
if not repre_ids:
if not containers:
return
project_name = legacy_io.active_project()
# Find representations based on found containers
repre_docs = get_representations(
project_name,
repre_ids=repre_ids,
fields=["_id", "parent"]
)
# Store representations by id and collect version ids
repre_docs_by_id = {}
version_ids = set()
for repre_doc in repre_docs:
# Use stringed representation id to match value in containers
repre_id = str(repre_doc["_id"])
repre_docs_by_id[repre_id] = repre_doc
version_ids.add(repre_doc["parent"])
filter_result = filter_containers(containers, project_name)
for container in filter_result.latest:
set_track_color(container["_track_item"], clip_color)
version_docs = get_versions(
project_name, version_ids, fields=["_id", "name", "parent"]
)
# Store versions by id and collect subset ids
version_docs_by_id = {}
subset_ids = set()
for version_doc in version_docs:
version_docs_by_id[version_doc["_id"]] = version_doc
subset_ids.add(version_doc["parent"])
# Query last versions based on subset ids
last_versions_by_subset_id = get_last_versions(
project_name, subset_ids=subset_ids, fields=["_id", "parent"]
)
for item in item_with_repre_id:
# Some python versions of nuke can't unfold tuple in for loop
track_item, repre_id = item
repre_doc = repre_docs_by_id[repre_id]
version_doc = version_docs_by_id[repre_doc["parent"]]
last_version_doc = last_versions_by_subset_id[version_doc["parent"]]
# Check if last version is same as current version
if version_doc["_id"] == last_version_doc["_id"]:
track_item.source().binItem().setColor(clip_color_last)
else:
track_item.source().binItem().setColor(clip_color)
for container in filter_result.outdated:
set_track_color(container["_track_item"], clip_color_last)
def selection_changed_timeline(event):

View file

@ -1,7 +1,7 @@
import pyblish.api
import openpype.api
import hou
from openpype.pipeline.publish import RepairAction
from openpype.hosts.houdini.api import lib
@ -13,7 +13,7 @@ class CollectRemotePublishSettings(pyblish.api.ContextPlugin):
hosts = ["houdini"]
targets = ["deadline"]
label = "Remote Publish Submission Settings"
actions = [openpype.api.RepairAction]
actions = [RepairAction]
def process(self, context):

View file

@ -1,11 +1,12 @@
import os
import pyblish.api
import openpype.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
class ExtractAlembic(openpype.api.Extractor):
class ExtractAlembic(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract Alembic"

View file

@ -1,11 +1,12 @@
import os
import pyblish.api
import openpype.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
class ExtractAss(openpype.api.Extractor):
class ExtractAss(publish.Extractor):
order = pyblish.api.ExtractorOrder + 0.1
label = "Extract Ass"

View file

@ -1,12 +1,12 @@
import os
import pyblish.api
import openpype.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
class ExtractComposite(openpype.api.Extractor):
class ExtractComposite(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract Composite (Image Sequence)"

View file

@ -4,10 +4,11 @@ import os
from pprint import pformat
import pyblish.api
import openpype.api
from openpype.pipeline import publish
class ExtractHDA(openpype.api.Extractor):
class ExtractHDA(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract HDA"

View file

@ -1,11 +1,12 @@
import os
import pyblish.api
import openpype.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
class ExtractRedshiftProxy(openpype.api.Extractor):
class ExtractRedshiftProxy(publish.Extractor):
order = pyblish.api.ExtractorOrder + 0.1
label = "Extract Redshift Proxy"

View file

@ -1,11 +1,12 @@
import os
import pyblish.api
import openpype.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
class ExtractUSD(openpype.api.Extractor):
class ExtractUSD(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract USD"

View file

@ -5,7 +5,6 @@ import sys
from collections import deque
import pyblish.api
import openpype.api
from openpype.client import (
get_asset_by_name,
@ -16,6 +15,7 @@ from openpype.client import (
from openpype.pipeline import (
get_representation_path,
legacy_io,
publish,
)
import openpype.hosts.houdini.api.usd as hou_usdlib
from openpype.hosts.houdini.api.lib import render_rop
@ -160,7 +160,7 @@ def parm_values(overrides):
parm.set(value)
class ExtractUSDLayered(openpype.api.Extractor):
class ExtractUSDLayered(publish.Extractor):
order = pyblish.api.ExtractorOrder
label = "Extract Layered USD"

View file

@ -1,11 +1,12 @@
import os
import pyblish.api
import openpype.api
from openpype.pipeline import publish
from openpype.hosts.houdini.api.lib import render_rop
class ExtractVDBCache(openpype.api.Extractor):
class ExtractVDBCache(publish.Extractor):
order = pyblish.api.ExtractorOrder + 0.1
label = "Extract VDB Cache"

View file

@ -1,253 +0,0 @@
import json
from collections import OrderedDict
import maya.cmds as cmds
import qargparse
from openpype.tools.utils.widgets import OptionDialog
from .lib import get_main_window, imprint
# To change as enum
build_types = ["context_asset", "linked_asset", "all_assets"]
def get_placeholder_attributes(node):
return {
attr: cmds.getAttr("{}.{}".format(node, attr))
for attr in cmds.listAttr(node, userDefined=True)}
def delete_placeholder_attributes(node):
'''
function to delete all extra placeholder attributes
'''
extra_attributes = get_placeholder_attributes(node)
for attribute in extra_attributes:
cmds.deleteAttr(node + '.' + attribute)
def create_placeholder():
args = placeholder_window()
if not args:
return # operation canceled, no locator created
# custom arg parse to force empty data query
# and still imprint them on placeholder
# and getting items when arg is of type Enumerator
options = create_options(args)
# create placeholder name dynamically from args and options
placeholder_name = create_placeholder_name(args, options)
selection = cmds.ls(selection=True)
if not selection:
raise ValueError("Nothing is selected")
placeholder = cmds.spaceLocator(name=placeholder_name)[0]
# get the long name of the placeholder (with the groups)
placeholder_full_name = cmds.ls(selection[0], long=True)[
0] + '|' + placeholder.replace('|', '')
if selection:
cmds.parent(placeholder, selection[0])
imprint(placeholder_full_name, options)
# Some tweaks because imprint force enums to to default value so we get
# back arg read and force them to attributes
imprint_enum(placeholder_full_name, args)
# Add helper attributes to keep placeholder info
cmds.addAttr(
placeholder_full_name,
longName="parent",
hidden=True,
dataType="string"
)
cmds.addAttr(
placeholder_full_name,
longName="index",
hidden=True,
attributeType="short",
defaultValue=-1
)
cmds.setAttr(placeholder_full_name + '.parent', "", type="string")
def create_placeholder_name(args, options):
placeholder_builder_type = [
arg.read() for arg in args if 'builder_type' in str(arg)
][0]
placeholder_family = options['family']
placeholder_name = placeholder_builder_type.split('_')
# add famlily in any
if placeholder_family:
placeholder_name.insert(1, placeholder_family)
# add loader arguments if any
if options['loader_args']:
pos = 2
loader_args = options['loader_args'].replace('\'', '\"')
loader_args = json.loads(loader_args)
values = [v for v in loader_args.values()]
for i in range(len(values)):
placeholder_name.insert(i + pos, values[i])
placeholder_name = '_'.join(placeholder_name)
return placeholder_name.capitalize()
def update_placeholder():
placeholder = cmds.ls(selection=True)
if len(placeholder) == 0:
raise ValueError("No node selected")
if len(placeholder) > 1:
raise ValueError("Too many selected nodes")
placeholder = placeholder[0]
args = placeholder_window(get_placeholder_attributes(placeholder))
if not args:
return # operation canceled
# delete placeholder attributes
delete_placeholder_attributes(placeholder)
options = create_options(args)
imprint(placeholder, options)
imprint_enum(placeholder, args)
cmds.addAttr(
placeholder,
longName="parent",
hidden=True,
dataType="string"
)
cmds.addAttr(
placeholder,
longName="index",
hidden=True,
attributeType="short",
defaultValue=-1
)
cmds.setAttr(placeholder + '.parent', '', type="string")
def create_options(args):
options = OrderedDict()
for arg in args:
if not type(arg) == qargparse.Separator:
options[str(arg)] = arg._data.get("items") or arg.read()
return options
def imprint_enum(placeholder, args):
"""
Imprint method doesn't act properly with enums.
Replacing the functionnality with this for now
"""
enum_values = {str(arg): arg.read()
for arg in args if arg._data.get("items")}
string_to_value_enum_table = {
build: i for i, build
in enumerate(build_types)}
for key, value in enum_values.items():
cmds.setAttr(
placeholder + "." + key,
string_to_value_enum_table[value])
def placeholder_window(options=None):
options = options or dict()
dialog = OptionDialog(parent=get_main_window())
dialog.setWindowTitle("Create Placeholder")
args = [
qargparse.Separator("Main attributes"),
qargparse.Enum(
"builder_type",
label="Asset Builder Type",
default=options.get("builder_type", 0),
items=build_types,
help="""Asset Builder Type
Builder type describe what template loader will look for.
context_asset : Template loader will look for subsets of
current context asset (Asset bob will find asset)
linked_asset : Template loader will look for assets linked
to current context asset.
Linked asset are looked in avalon database under field "inputLinks"
"""
),
qargparse.String(
"family",
default=options.get("family", ""),
label="OpenPype Family",
placeholder="ex: model, look ..."),
qargparse.String(
"representation",
default=options.get("representation", ""),
label="OpenPype Representation",
placeholder="ex: ma, abc ..."),
qargparse.String(
"loader",
default=options.get("loader", ""),
label="Loader",
placeholder="ex: ReferenceLoader, LightLoader ...",
help="""Loader
Defines what openpype loader will be used to load assets.
Useable loader depends on current host's loader list.
Field is case sensitive.
"""),
qargparse.String(
"loader_args",
default=options.get("loader_args", ""),
label="Loader Arguments",
placeholder='ex: {"camera":"persp", "lights":True}',
help="""Loader
Defines a dictionnary of arguments used to load assets.
Useable arguments depend on current placeholder Loader.
Field should be a valid python dict. Anything else will be ignored.
"""),
qargparse.Integer(
"order",
default=options.get("order", 0),
min=0,
max=999,
label="Order",
placeholder="ex: 0, 100 ... (smallest order loaded first)",
help="""Order
Order defines asset loading priority (0 to 999)
Priority rule is : "lowest is first to load"."""),
qargparse.Separator(
"Optional attributes"),
qargparse.String(
"asset",
default=options.get("asset", ""),
label="Asset filter",
placeholder="regex filtering by asset name",
help="Filtering assets by matching field regex to asset's name"),
qargparse.String(
"subset",
default=options.get("subset", ""),
label="Subset filter",
placeholder="regex filtering by subset name",
help="Filtering assets by matching field regex to subset's name"),
qargparse.String(
"hierarchy",
default=options.get("hierarchy", ""),
label="Hierarchy filter",
placeholder="regex filtering by asset's hierarchy",
help="Filtering assets by matching field asset's hierarchy")
]
dialog.create(args)
if not dialog.exec_():
return None
return args

View file

@ -9,16 +9,17 @@ import maya.cmds as cmds
from openpype.settings import get_project_settings
from openpype.pipeline import legacy_io
from openpype.pipeline.workfile import BuildWorkfile
from openpype.pipeline.workfile.build_template import (
build_workfile_template,
update_workfile_template
)
from openpype.tools.utils import host_tools
from openpype.hosts.maya.api import lib, lib_rendersettings
from .lib import get_main_window, IS_HEADLESS
from .commands import reset_frame_range
from .lib_template_builder import create_placeholder, update_placeholder
from .workfile_template_builder import (
create_placeholder,
update_placeholder,
build_workfile_template,
update_workfile_template,
)
log = logging.getLogger(__name__)
@ -161,12 +162,12 @@ def install():
cmds.menuItem(
"Create Placeholder",
parent=builder_menu,
command=lambda *args: create_placeholder()
command=create_placeholder
)
cmds.menuItem(
"Update Placeholder",
parent=builder_menu,
command=lambda *args: update_placeholder()
command=update_placeholder
)
cmds.menuItem(
"Build Workfile from template",

View file

@ -42,6 +42,7 @@ from openpype.hosts.maya import MAYA_ROOT_DIR
from openpype.hosts.maya.lib import create_workspace_mel
from . import menu, lib
from .workfile_template_builder import MayaPlaceholderLoadPlugin
from .workio import (
open_file,
save_file,
@ -135,6 +136,11 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost):
def get_containers(self):
return ls()
def get_workfile_build_placeholder_plugins(self):
return [
MayaPlaceholderLoadPlugin
]
@contextlib.contextmanager
def maintained_selection(self):
with lib.maintained_selection():

View file

@ -1,252 +0,0 @@
import re
from maya import cmds
from openpype.client import get_representations
from openpype.pipeline import legacy_io
from openpype.pipeline.workfile.abstract_template_loader import (
AbstractPlaceholder,
AbstractTemplateLoader
)
from openpype.pipeline.workfile.build_template_exceptions import (
TemplateAlreadyImported
)
PLACEHOLDER_SET = 'PLACEHOLDERS_SET'
class MayaTemplateLoader(AbstractTemplateLoader):
"""Concrete implementation of AbstractTemplateLoader for maya
"""
def import_template(self, path):
"""Import template into current scene.
Block if a template is already loaded.
Args:
path (str): A path to current template (usually given by
get_template_path implementation)
Returns:
bool: Wether the template was succesfully imported or not
"""
if cmds.objExists(PLACEHOLDER_SET):
raise TemplateAlreadyImported(
"Build template already loaded\n"
"Clean scene if needed (File > New Scene)")
cmds.sets(name=PLACEHOLDER_SET, empty=True)
self.new_nodes = cmds.file(path, i=True, returnNewNodes=True)
cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True)
for set in cmds.listSets(allSets=True):
if (cmds.objExists(set) and
cmds.attributeQuery('id', node=set, exists=True) and
cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'):
if cmds.attributeQuery('asset', node=set, exists=True):
cmds.setAttr(
set + '.asset',
legacy_io.Session['AVALON_ASSET'], type='string'
)
return True
def template_already_imported(self, err_msg):
clearButton = "Clear scene and build"
updateButton = "Update template"
abortButton = "Abort"
title = "Scene already builded"
message = (
"It's seems a template was already build for this scene.\n"
"Error message reveived :\n\n\"{}\"".format(err_msg))
buttons = [clearButton, updateButton, abortButton]
defaultButton = clearButton
cancelButton = abortButton
dismissString = abortButton
answer = cmds.confirmDialog(
t=title,
m=message,
b=buttons,
db=defaultButton,
cb=cancelButton,
ds=dismissString)
if answer == clearButton:
cmds.file(newFile=True, force=True)
self.import_template(self.template_path)
self.populate_template()
elif answer == updateButton:
self.update_missing_containers()
elif answer == abortButton:
return
@staticmethod
def get_template_nodes():
attributes = cmds.ls('*.builder_type', long=True)
return [attribute.rpartition('.')[0] for attribute in attributes]
def get_loaded_containers_by_id(self):
try:
containers = cmds.sets("AVALON_CONTAINERS", q=True)
except ValueError:
return None
return [
cmds.getAttr(container + '.representation')
for container in containers]
class MayaPlaceholder(AbstractPlaceholder):
"""Concrete implementation of AbstractPlaceholder for maya
"""
optional_keys = {'asset', 'subset', 'hierarchy'}
def get_data(self, node):
user_data = dict()
for attr in self.required_keys.union(self.optional_keys):
attribute_name = '{}.{}'.format(node, attr)
if not cmds.attributeQuery(attr, node=node, exists=True):
print("{} not found".format(attribute_name))
continue
user_data[attr] = cmds.getAttr(
attribute_name,
asString=True)
user_data['parent'] = (
cmds.getAttr(node + '.parent', asString=True)
or node.rpartition('|')[0]
or ""
)
user_data['node'] = node
if user_data['parent']:
siblings = cmds.listRelatives(user_data['parent'], children=True)
else:
siblings = cmds.ls(assemblies=True)
node_shortname = user_data['node'].rpartition('|')[2]
current_index = cmds.getAttr(node + '.index', asString=True)
user_data['index'] = (
current_index if current_index >= 0
else siblings.index(node_shortname))
self.data = user_data
def parent_in_hierarchy(self, containers):
"""Parent loaded container to placeholder's parent
ie : Set loaded content as placeholder's sibling
Args:
containers (String): Placeholder loaded containers
"""
if not containers:
return
roots = cmds.sets(containers, q=True)
nodes_to_parent = []
for root in roots:
if root.endswith("_RN"):
refRoot = cmds.referenceQuery(root, n=True)[0]
refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot]
nodes_to_parent.extend(refRoot)
elif root in cmds.listSets(allSets=True):
if not cmds.sets(root, q=True):
return
else:
continue
else:
nodes_to_parent.append(root)
if self.data['parent']:
cmds.parent(nodes_to_parent, self.data['parent'])
# Move loaded nodes to correct index in outliner hierarchy
placeholder_node = self.data['node']
placeholder_form = cmds.xform(
placeholder_node,
q=True,
matrix=True,
worldSpace=True
)
for node in set(nodes_to_parent):
cmds.reorder(node, front=True)
cmds.reorder(node, relative=self.data['index'])
cmds.xform(node, matrix=placeholder_form, ws=True)
holding_sets = cmds.listSets(object=placeholder_node)
if not holding_sets:
return
for holding_set in holding_sets:
cmds.sets(roots, forceElement=holding_set)
def clean(self):
"""Hide placeholder, parent them to root
add them to placeholder set and register placeholder's parent
to keep placeholder info available for future use
"""
node = self.data['node']
if self.data['parent']:
cmds.setAttr(node + '.parent', self.data['parent'], type='string')
if cmds.getAttr(node + '.index') < 0:
cmds.setAttr(node + '.index', self.data['index'])
holding_sets = cmds.listSets(object=node)
if holding_sets:
for set in holding_sets:
cmds.sets(node, remove=set)
if cmds.listRelatives(node, p=True):
node = cmds.parent(node, world=True)[0]
cmds.sets(node, addElement=PLACEHOLDER_SET)
cmds.hide(node)
cmds.setAttr(node + '.hiddenInOutliner', True)
def get_representations(self, current_asset_doc, linked_asset_docs):
project_name = legacy_io.active_project()
builder_type = self.data["builder_type"]
if builder_type == "context_asset":
context_filters = {
"asset": [current_asset_doc["name"]],
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representations": [self.data["representation"]],
"family": [self.data["family"]]
}
elif builder_type != "linked_asset":
context_filters = {
"asset": [re.compile(self.data["asset"])],
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representation": [self.data["representation"]],
"family": [self.data["family"]]
}
else:
asset_regex = re.compile(self.data["asset"])
linked_asset_names = []
for asset_doc in linked_asset_docs:
asset_name = asset_doc["name"]
if asset_regex.match(asset_name):
linked_asset_names.append(asset_name)
context_filters = {
"asset": linked_asset_names,
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representation": [self.data["representation"]],
"family": [self.data["family"]],
}
return list(get_representations(
project_name,
context_filters=context_filters
))
def err_message(self):
return (
"Error while trying to load a representation.\n"
"Either the subset wasn't published or the template is malformed."
"\n\n"
"Builder was looking for :\n{attributes}".format(
attributes="\n".join([
"{}: {}".format(key.title(), value)
for key, value in self.data.items()]
)
)
)

View file

@ -0,0 +1,330 @@
import json
from maya import cmds
from openpype.pipeline import registered_host
from openpype.pipeline.workfile.workfile_template_builder import (
TemplateAlreadyImported,
AbstractTemplateBuilder,
PlaceholderPlugin,
LoadPlaceholderItem,
PlaceholderLoadMixin,
)
from openpype.tools.workfile_template_build import (
WorkfileBuildPlaceholderDialog,
)
from .lib import read, imprint
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
class MayaTemplateBuilder(AbstractTemplateBuilder):
"""Concrete implementation of AbstractTemplateBuilder for maya"""
def import_template(self, path):
"""Import template into current scene.
Block if a template is already loaded.
Args:
path (str): A path to current template (usually given by
get_template_path implementation)
Returns:
bool: Wether the template was succesfully imported or not
"""
if cmds.objExists(PLACEHOLDER_SET):
raise TemplateAlreadyImported((
"Build template already loaded\n"
"Clean scene if needed (File > New Scene)"
))
cmds.sets(name=PLACEHOLDER_SET, empty=True)
cmds.file(path, i=True, returnNewNodes=True)
cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True)
return True
class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin):
identifier = "maya.load"
label = "Maya load"
def _collect_scene_placeholders(self):
# Cache placeholder data to shared data
placeholder_nodes = self.builder.get_shared_populate_data(
"placeholder_nodes"
)
if placeholder_nodes is None:
attributes = cmds.ls("*.plugin_identifier", long=True)
placeholder_nodes = {}
for attribute in attributes:
node_name = attribute.rpartition(".")[0]
placeholder_nodes[node_name] = (
self._parse_placeholder_node_data(node_name)
)
self.builder.set_shared_populate_data(
"placeholder_nodes", placeholder_nodes
)
return placeholder_nodes
def _parse_placeholder_node_data(self, node_name):
placeholder_data = read(node_name)
parent_name = (
cmds.getAttr(node_name + ".parent", asString=True)
or node_name.rpartition("|")[0]
or ""
)
if parent_name:
siblings = cmds.listRelatives(parent_name, children=True)
else:
siblings = cmds.ls(assemblies=True)
node_shortname = node_name.rpartition("|")[2]
current_index = cmds.getAttr(node_name + ".index", asString=True)
if current_index < 0:
current_index = siblings.index(node_shortname)
placeholder_data.update({
"parent": parent_name,
"index": current_index
})
return placeholder_data
def _create_placeholder_name(self, placeholder_data):
placeholder_name_parts = placeholder_data["builder_type"].split("_")
pos = 1
# add famlily in any
placeholder_family = placeholder_data["family"]
if placeholder_family:
placeholder_name_parts.insert(pos, placeholder_family)
pos += 1
# add loader arguments if any
loader_args = placeholder_data["loader_args"]
if loader_args:
loader_args = json.loads(loader_args.replace('\'', '\"'))
values = [v for v in loader_args.values()]
for value in values:
placeholder_name_parts.insert(pos, value)
pos += 1
placeholder_name = "_".join(placeholder_name_parts)
return placeholder_name.capitalize()
def _get_loaded_repre_ids(self):
loaded_representation_ids = self.builder.get_shared_populate_data(
"loaded_representation_ids"
)
if loaded_representation_ids is None:
try:
containers = cmds.sets("AVALON_CONTAINERS", q=True)
except ValueError:
containers = []
loaded_representation_ids = {
cmds.getAttr(container + ".representation")
for container in containers
}
self.builder.set_shared_populate_data(
"loaded_representation_ids", loaded_representation_ids
)
return loaded_representation_ids
def create_placeholder(self, placeholder_data):
selection = cmds.ls(selection=True)
if not selection:
raise ValueError("Nothing is selected")
if len(selection) > 1:
raise ValueError("More then one item are selected")
placeholder_data["plugin_identifier"] = self.identifier
placeholder_name = self._create_placeholder_name(placeholder_data)
placeholder = cmds.spaceLocator(name=placeholder_name)[0]
# TODO: this can crash if selection can't be used
cmds.parent(placeholder, selection[0])
# get the long name of the placeholder (with the groups)
placeholder_full_name = (
cmds.ls(selection[0], long=True)[0]
+ "|"
+ placeholder.replace("|", "")
)
imprint(placeholder_full_name, placeholder_data)
# Add helper attributes to keep placeholder info
cmds.addAttr(
placeholder_full_name,
longName="parent",
hidden=True,
dataType="string"
)
cmds.addAttr(
placeholder_full_name,
longName="index",
hidden=True,
attributeType="short",
defaultValue=-1
)
cmds.setAttr(placeholder_full_name + ".parent", "", type="string")
def update_placeholder(self, placeholder_item, placeholder_data):
node_name = placeholder_item.scene_identifier
new_values = {}
for key, value in placeholder_data.items():
placeholder_value = placeholder_item.data.get(key)
if value != placeholder_value:
new_values[key] = value
placeholder_item.data[key] = value
for key in new_values.keys():
cmds.deleteAttr(node_name + "." + key)
imprint(node_name, new_values)
def collect_placeholders(self):
output = []
scene_placeholders = self._collect_scene_placeholders()
for node_name, placeholder_data in scene_placeholders.items():
if placeholder_data.get("plugin_identifier") != self.identifier:
continue
# TODO do data validations and maybe updgrades if are invalid
output.append(
LoadPlaceholderItem(node_name, placeholder_data, self)
)
return output
def populate_placeholder(self, placeholder):
self.populate_load_placeholder(placeholder)
def repopulate_placeholder(self, placeholder):
repre_ids = self._get_loaded_repre_ids()
self.populate_load_placeholder(placeholder, repre_ids)
def get_placeholder_options(self, options=None):
return self.get_load_plugin_options(options)
def cleanup_placeholder(self, placeholder, failed):
"""Hide placeholder, parent them to root
add them to placeholder set and register placeholder's parent
to keep placeholder info available for future use
"""
node = placeholder._scene_identifier
node_parent = placeholder.data["parent"]
if node_parent:
cmds.setAttr(node + ".parent", node_parent, type="string")
if cmds.getAttr(node + ".index") < 0:
cmds.setAttr(node + ".index", placeholder.data["index"])
holding_sets = cmds.listSets(object=node)
if holding_sets:
for set in holding_sets:
cmds.sets(node, remove=set)
if cmds.listRelatives(node, p=True):
node = cmds.parent(node, world=True)[0]
cmds.sets(node, addElement=PLACEHOLDER_SET)
cmds.hide(node)
cmds.setAttr(node + ".hiddenInOutliner", True)
def load_succeed(self, placeholder, container):
self._parent_in_hierarhchy(placeholder, container)
def _parent_in_hierarchy(self, placeholder, container):
"""Parent loaded container to placeholder's parent.
ie : Set loaded content as placeholder's sibling
Args:
container (str): Placeholder loaded containers
"""
if not container:
return
roots = cmds.sets(container, q=True)
nodes_to_parent = []
for root in roots:
if root.endswith("_RN"):
refRoot = cmds.referenceQuery(root, n=True)[0]
refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot]
nodes_to_parent.extend(refRoot)
elif root not in cmds.listSets(allSets=True):
nodes_to_parent.append(root)
elif not cmds.sets(root, q=True):
return
if placeholder.data["parent"]:
cmds.parent(nodes_to_parent, placeholder.data["parent"])
# Move loaded nodes to correct index in outliner hierarchy
placeholder_form = cmds.xform(
placeholder.scene_identifier,
q=True,
matrix=True,
worldSpace=True
)
for node in set(nodes_to_parent):
cmds.reorder(node, front=True)
cmds.reorder(node, relative=placeholder.data["index"])
cmds.xform(node, matrix=placeholder_form, ws=True)
holding_sets = cmds.listSets(object=placeholder.scene_identifier)
if not holding_sets:
return
for holding_set in holding_sets:
cmds.sets(roots, forceElement=holding_set)
def build_workfile_template(*args):
builder = MayaTemplateBuilder(registered_host())
builder.build_template()
def update_workfile_template(*args):
builder = MayaTemplateBuilder(registered_host())
builder.rebuild_template()
def create_placeholder(*args):
host = registered_host()
builder = MayaTemplateBuilder(host)
window = WorkfileBuildPlaceholderDialog(host, builder)
window.exec_()
def update_placeholder(*args):
host = registered_host()
builder = MayaTemplateBuilder(host)
placeholder_items_by_id = {
placeholder_item.scene_identifier: placeholder_item
for placeholder_item in builder.get_placeholders()
}
placeholder_items = []
for node_name in cmds.ls(selection=True, long=True):
if node_name in placeholder_items_by_id:
placeholder_items.append(placeholder_items_by_id[node_name])
# TODO show UI at least
if len(placeholder_items) == 0:
raise ValueError("No node selected")
if len(placeholder_items) > 1:
raise ValueError("Too many selected nodes")
placeholder_item = placeholder_items[0]
window = WorkfileBuildPlaceholderDialog(host, builder)
window.set_update_mode(placeholder_item)
window.exec_()

View file

@ -13,6 +13,12 @@ from openpype.pipeline.publish import (
from openpype.hosts.maya.api import lib
def get_redshift_image_format_labels():
"""Return nice labels for Redshift image formats."""
var = "$g_redshiftImageFormatLabels"
return mel.eval("{0}={0}".format(var))
class ValidateRenderSettings(pyblish.api.InstancePlugin):
"""Validates the global render settings
@ -105,8 +111,9 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
# Get the node attributes for current renderer
attrs = lib.RENDER_ATTRS.get(renderer, lib.RENDER_ATTRS['default'])
# Prefix attribute can return None when a value was never set
prefix = lib.get_attr_in_layer(cls.ImagePrefixes[renderer],
layer=layer)
layer=layer) or ""
padding = lib.get_attr_in_layer("{node}.{padding}".format(**attrs),
layer=layer)
@ -183,18 +190,22 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
redshift_AOV_prefix
))
invalid = True
# get aov format
aov_ext = cmds.getAttr(
"{}.fileFormat".format(aov), asString=True)
default_ext = cmds.getAttr(
"redshiftOptions.imageFormat", asString=True)
# check aov file format
aov_ext = cmds.getAttr("{}.fileFormat".format(aov))
default_ext = cmds.getAttr("redshiftOptions.imageFormat")
aov_type = cmds.getAttr("{}.aovType".format(aov))
if aov_type == "Cryptomatte":
# redshift Cryptomatte AOV always uses "Cryptomatte (EXR)"
# so we ignore validating file format for it.
pass
if default_ext != aov_ext:
cls.log.error(("AOV file format is not the same "
"as the one set globally "
"{} != {}").format(default_ext,
aov_ext))
elif default_ext != aov_ext:
labels = get_redshift_image_format_labels()
cls.log.error(
"AOV file format {} does not match global file format "
"{}".format(labels[aov_ext], labels[default_ext])
)
invalid = True
if renderer == "renderman":
@ -302,6 +313,9 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
default = lib.RENDER_ATTRS['default']
render_attrs = lib.RENDER_ATTRS.get(renderer, default)
# Repair animation must be enabled
cmds.setAttr("defaultRenderGlobals.animation", True)
# Repair prefix
if renderer != "renderman":
node = render_attrs["node"]
@ -334,8 +348,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin):
cmds.optionMenuGrp("vrayRenderElementSeparator",
v=instance.data.get("aovSeparator", "_"))
cmds.setAttr(
"{}.fileNameRenderElementSeparator".format(
node),
"{}.fileNameRenderElementSeparator".format(node),
instance.data.get("aovSeparator", "_"),
type="string"
)

View file

@ -21,6 +21,8 @@ from .pipeline import (
containerise,
parse_container,
update_container,
get_workfile_build_placeholder_plugins,
)
from .lib import (
maintained_selection,
@ -55,6 +57,8 @@ __all__ = (
"parse_container",
"update_container",
"get_workfile_build_placeholder_plugins",
"maintained_selection",
"reset_selection",
"get_view_process_node",

View file

@ -1,220 +0,0 @@
from collections import OrderedDict
import qargparse
import nuke
from openpype.tools.utils.widgets import OptionDialog
from .lib import imprint, get_main_window
# To change as enum
build_types = ["context_asset", "linked_asset", "all_assets"]
def get_placeholder_attributes(node, enumerate=False):
list_atts = {
"builder_type",
"family",
"representation",
"loader",
"loader_args",
"order",
"asset",
"subset",
"hierarchy",
"siblings",
"last_loaded"
}
attributes = {}
for attr in node.knobs().keys():
if attr in list_atts:
if enumerate:
try:
attributes[attr] = node.knob(attr).values()
except AttributeError:
attributes[attr] = node.knob(attr).getValue()
else:
attributes[attr] = node.knob(attr).getValue()
return attributes
def delete_placeholder_attributes(node):
"""Delete all extra placeholder attributes."""
extra_attributes = get_placeholder_attributes(node)
for attribute in extra_attributes.keys():
try:
node.removeKnob(node.knob(attribute))
except ValueError:
continue
def hide_placeholder_attributes(node):
"""Hide all extra placeholder attributes."""
extra_attributes = get_placeholder_attributes(node)
for attribute in extra_attributes.keys():
try:
node.knob(attribute).setVisible(False)
except ValueError:
continue
def create_placeholder():
args = placeholder_window()
if not args:
# operation canceled, no locator created
return
placeholder = nuke.nodes.NoOp()
placeholder.setName("PLACEHOLDER")
placeholder.knob("tile_color").setValue(4278190335)
# custom arg parse to force empty data query
# and still imprint them on placeholder
# and getting items when arg is of type Enumerator
options = OrderedDict()
for arg in args:
if not type(arg) == qargparse.Separator:
options[str(arg)] = arg._data.get("items") or arg.read()
imprint(placeholder, options)
imprint(placeholder, {"is_placeholder": True})
placeholder.knob("is_placeholder").setVisible(False)
def update_placeholder():
placeholder = nuke.selectedNodes()
if not placeholder:
raise ValueError("No node selected")
if len(placeholder) > 1:
raise ValueError("Too many selected nodes")
placeholder = placeholder[0]
args = placeholder_window(get_placeholder_attributes(placeholder))
if not args:
return # operation canceled
# delete placeholder attributes
delete_placeholder_attributes(placeholder)
options = OrderedDict()
for arg in args:
if not type(arg) == qargparse.Separator:
options[str(arg)] = arg._data.get("items") or arg.read()
imprint(placeholder, options)
def imprint_enum(placeholder, args):
"""
Imprint method doesn't act properly with enums.
Replacing the functionnality with this for now
"""
enum_values = {
str(arg): arg.read()
for arg in args
if arg._data.get("items")
}
string_to_value_enum_table = {
build: idx
for idx, build in enumerate(build_types)
}
attrs = {}
for key, value in enum_values.items():
attrs[key] = string_to_value_enum_table[value]
def placeholder_window(options=None):
options = options or dict()
dialog = OptionDialog(parent=get_main_window())
dialog.setWindowTitle("Create Placeholder")
args = [
qargparse.Separator("Main attributes"),
qargparse.Enum(
"builder_type",
label="Asset Builder Type",
default=options.get("builder_type", 0),
items=build_types,
help="""Asset Builder Type
Builder type describe what template loader will look for.
context_asset : Template loader will look for subsets of
current context asset (Asset bob will find asset)
linked_asset : Template loader will look for assets linked
to current context asset.
Linked asset are looked in OpenPype database under field "inputLinks"
"""
),
qargparse.String(
"family",
default=options.get("family", ""),
label="OpenPype Family",
placeholder="ex: image, plate ..."),
qargparse.String(
"representation",
default=options.get("representation", ""),
label="OpenPype Representation",
placeholder="ex: mov, png ..."),
qargparse.String(
"loader",
default=options.get("loader", ""),
label="Loader",
placeholder="ex: LoadClip, LoadImage ...",
help="""Loader
Defines what openpype loader will be used to load assets.
Useable loader depends on current host's loader list.
Field is case sensitive.
"""),
qargparse.String(
"loader_args",
default=options.get("loader_args", ""),
label="Loader Arguments",
placeholder='ex: {"camera":"persp", "lights":True}',
help="""Loader
Defines a dictionnary of arguments used to load assets.
Useable arguments depend on current placeholder Loader.
Field should be a valid python dict. Anything else will be ignored.
"""),
qargparse.Integer(
"order",
default=options.get("order", 0),
min=0,
max=999,
label="Order",
placeholder="ex: 0, 100 ... (smallest order loaded first)",
help="""Order
Order defines asset loading priority (0 to 999)
Priority rule is : "lowest is first to load"."""),
qargparse.Separator(
"Optional attributes "),
qargparse.String(
"asset",
default=options.get("asset", ""),
label="Asset filter",
placeholder="regex filtering by asset name",
help="Filtering assets by matching field regex to asset's name"),
qargparse.String(
"subset",
default=options.get("subset", ""),
label="Subset filter",
placeholder="regex filtering by subset name",
help="Filtering assets by matching field regex to subset's name"),
qargparse.String(
"hierarchy",
default=options.get("hierarchy", ""),
label="Hierarchy filter",
placeholder="regex filtering by asset's hierarchy",
help="Filtering assets by matching field asset's hierarchy")
]
dialog.create(args)
if not dialog.exec_():
return None
return args

View file

@ -22,10 +22,6 @@ from openpype.pipeline import (
AVALON_CONTAINER_ID,
)
from openpype.pipeline.workfile import BuildWorkfile
from openpype.pipeline.workfile.build_template import (
build_workfile_template,
update_workfile_template
)
from openpype.tools.utils import host_tools
from .command import viewer_update_and_undo_stop
@ -40,8 +36,12 @@ from .lib import (
set_avalon_knob_data,
read_avalon_data,
)
from .lib_template_builder import (
create_placeholder, update_placeholder
from .workfile_template_builder import (
NukePlaceholderLoadPlugin,
build_workfile_template,
update_workfile_template,
create_placeholder,
update_placeholder,
)
log = Logger.get_logger(__name__)
@ -141,6 +141,12 @@ def _show_workfiles():
host_tools.show_workfiles(parent=None, on_top=False)
def get_workfile_build_placeholder_plugins():
return [
NukePlaceholderLoadPlugin
]
def _install_menu():
# uninstall original avalon menu
main_window = get_main_window()

View file

@ -1,13 +1,16 @@
import re
import collections
import nuke
from openpype.client import get_representations
from openpype.pipeline import legacy_io
from openpype.pipeline.workfile.abstract_template_loader import (
AbstractPlaceholder,
AbstractTemplateLoader,
from openpype.pipeline import registered_host
from openpype.pipeline.workfile.workfile_template_builder import (
AbstractTemplateBuilder,
PlaceholderPlugin,
LoadPlaceholderItem,
PlaceholderLoadMixin,
)
from openpype.tools.workfile_template_build import (
WorkfileBuildPlaceholderDialog,
)
from .lib import (
@ -25,19 +28,11 @@ from .lib import (
node_tempfile,
)
from .lib_template_builder import (
delete_placeholder_attributes,
get_placeholder_attributes,
hide_placeholder_attributes
)
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
class NukeTemplateLoader(AbstractTemplateLoader):
"""Concrete implementation of AbstractTemplateLoader for Nuke
"""
class NukeTemplateBuilder(AbstractTemplateBuilder):
"""Concrete implementation of AbstractTemplateBuilder for maya"""
def import_template(self, path):
"""Import template into current scene.
@ -58,224 +53,255 @@ class NukeTemplateLoader(AbstractTemplateLoader):
return True
def preload(self, placeholder, loaders_by_name, last_representation):
placeholder.data["nodes_init"] = nuke.allNodes()
placeholder.data["last_repre_id"] = str(last_representation["_id"])
def populate_template(self, ignored_ids=None):
processed_key = "_node_processed"
class NukePlaceholderPlugin(PlaceholderPlugin):
node_color = 4278190335
processed_nodes = []
nodes = self.get_template_nodes()
while nodes:
# Mark nodes as processed so they're not re-executed
# - that can happen if processing of placeholder node fails
for node in nodes:
imprint(node, {processed_key: True})
processed_nodes.append(node)
def _collect_scene_placeholders(self):
# Cache placeholder data to shared data
placeholder_nodes = self.builder.get_shared_populate_data(
"placeholder_nodes"
)
if placeholder_nodes is None:
placeholder_nodes = {}
all_groups = collections.deque()
all_groups.append(nuke.thisGroup())
while all_groups:
group = all_groups.popleft()
for node in group.nodes():
if isinstance(node, nuke.Group):
all_groups.append(node)
super(NukeTemplateLoader, self).populate_template(ignored_ids)
node_knobs = node.knobs()
if (
"builder_type" not in node_knobs
or "is_placeholder" not in node_knobs
or not node.knob("is_placeholder").value()
):
continue
# Recollect nodes to repopulate
nodes = []
for node in self.get_template_nodes():
# Skip already processed nodes
if (
processed_key in node.knobs()
and node.knob(processed_key).value()
):
continue
nodes.append(node)
if "empty" in node_knobs and node.knob("empty").value():
continue
for node in processed_nodes:
knob = node.knob(processed_key)
placeholder_nodes[node.fullName()] = node
self.builder.set_shared_populate_data(
"placeholder_nodes", placeholder_nodes
)
return placeholder_nodes
def create_placeholder(self, placeholder_data):
placeholder_data["plugin_identifier"] = self.identifier
placeholder = nuke.nodes.NoOp()
placeholder.setName("PLACEHOLDER")
placeholder.knob("tile_color").setValue(self.node_color)
imprint(placeholder, placeholder_data)
imprint(placeholder, {"is_placeholder": True})
placeholder.knob("is_placeholder").setVisible(False)
def update_placeholder(self, placeholder_item, placeholder_data):
node = nuke.toNode(placeholder_item.scene_identifier)
imprint(node, placeholder_data)
def _parse_placeholder_node_data(self, node):
placeholder_data = {}
for key in self.get_placeholder_keys():
knob = node.knob(key)
value = None
if knob is not None:
node.removeKnob(knob)
@staticmethod
def get_template_nodes():
placeholders = []
all_groups = collections.deque()
all_groups.append(nuke.thisGroup())
while all_groups:
group = all_groups.popleft()
for node in group.nodes():
if isinstance(node, nuke.Group):
all_groups.append(node)
node_knobs = node.knobs()
if (
"builder_type" not in node_knobs
or "is_placeholder" not in node_knobs
or not node.knob("is_placeholder").value()
):
continue
if "empty" in node_knobs and node.knob("empty").value():
continue
placeholders.append(node)
return placeholders
def update_missing_containers(self):
nodes_by_id = collections.defaultdict(list)
for node in nuke.allNodes():
node_knobs = node.knobs().keys()
if "repre_id" in node_knobs:
repre_id = node.knob("repre_id").getValue()
nodes_by_id[repre_id].append(node.name())
if "empty" in node_knobs:
node.removeKnob(node.knob("empty"))
imprint(node, {"empty": False})
for node_names in nodes_by_id.values():
node = None
for node_name in node_names:
node_by_name = nuke.toNode(node_name)
if "builder_type" in node_by_name.knobs().keys():
node = node_by_name
break
if node is None:
continue
placeholder = nuke.nodes.NoOp()
placeholder.setName("PLACEHOLDER")
placeholder.knob("tile_color").setValue(4278190335)
attributes = get_placeholder_attributes(node, enumerate=True)
imprint(placeholder, attributes)
pos_x = int(node.knob("x").getValue())
pos_y = int(node.knob("y").getValue())
placeholder.setXYpos(pos_x, pos_y)
imprint(placeholder, {"nb_children": 1})
refresh_node(placeholder)
self.populate_template(self.get_loaded_containers_by_id())
def get_loaded_containers_by_id(self):
repre_ids = set()
for node in nuke.allNodes():
if "repre_id" in node.knobs():
repre_ids.add(node.knob("repre_id").getValue())
# Removes duplicates in the list
return list(repre_ids)
def delete_placeholder(self, placeholder):
placeholder_node = placeholder.data["node"]
last_loaded = placeholder.data["last_loaded"]
if not placeholder.data["delete"]:
if "empty" in placeholder_node.knobs().keys():
placeholder_node.removeKnob(placeholder_node.knob("empty"))
imprint(placeholder_node, {"empty": True})
return
if not last_loaded:
nuke.delete(placeholder_node)
return
if "last_loaded" in placeholder_node.knobs().keys():
for node_name in placeholder_node.knob("last_loaded").values():
node = nuke.toNode(node_name)
try:
delete_placeholder_attributes(node)
except Exception:
pass
last_loaded_names = [
loaded_node.name()
for loaded_node in last_loaded
]
imprint(placeholder_node, {"last_loaded": last_loaded_names})
for node in last_loaded:
refresh_node(node)
refresh_node(placeholder_node)
if "builder_type" not in node.knobs().keys():
attributes = get_placeholder_attributes(placeholder_node, True)
imprint(node, attributes)
imprint(node, {"is_placeholder": False})
hide_placeholder_attributes(node)
node.knob("is_placeholder").setVisible(False)
imprint(
node,
{
"x": placeholder_node.xpos(),
"y": placeholder_node.ypos()
}
)
node.knob("x").setVisible(False)
node.knob("y").setVisible(False)
nuke.delete(placeholder_node)
value = knob.getValue()
placeholder_data[key] = value
return placeholder_data
class NukePlaceholder(AbstractPlaceholder):
"""Concrete implementation of AbstractPlaceholder for Nuke"""
class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin):
identifier = "nuke.load"
label = "Nuke load"
optional_keys = {"asset", "subset", "hierarchy"}
def _parse_placeholder_node_data(self, node):
placeholder_data = super(
NukePlaceholderLoadPlugin, self
)._parse_placeholder_node_data(node)
def get_data(self, node):
user_data = dict()
node_knobs = node.knobs()
for attr in self.required_keys.union(self.optional_keys):
if attr in node_knobs:
user_data[attr] = node_knobs[attr].getValue()
user_data["node"] = node
nb_children = 0
if "nb_children" in node_knobs:
nb_children = int(node_knobs["nb_children"].getValue())
user_data["nb_children"] = nb_children
placeholder_data["nb_children"] = nb_children
siblings = []
if "siblings" in node_knobs:
siblings = node_knobs["siblings"].values()
user_data["siblings"] = siblings
placeholder_data["siblings"] = siblings
node_full_name = node.fullName()
user_data["group_name"] = node_full_name.rpartition(".")[0]
user_data["last_loaded"] = []
user_data["delete"] = False
self.data = user_data
placeholder_data["group_name"] = node_full_name.rpartition(".")[0]
placeholder_data["last_loaded"] = []
placeholder_data["delete"] = False
return placeholder_data
def parent_in_hierarchy(self, containers):
return
def _get_loaded_repre_ids(self):
loaded_representation_ids = self.builder.get_shared_populate_data(
"loaded_representation_ids"
)
if loaded_representation_ids is None:
loaded_representation_ids = set()
for node in nuke.allNodes():
if "repre_id" in node.knobs():
loaded_representation_ids.add(
node.knob("repre_id").getValue()
)
def create_sib_copies(self):
""" creating copies of the palce_holder siblings (the ones who were
loaded with it) for the new nodes added
self.builder.set_shared_populate_data(
"loaded_representation_ids", loaded_representation_ids
)
return loaded_representation_ids
def _before_repre_load(self, placeholder, representation):
placeholder.data["nodes_init"] = nuke.allNodes()
placeholder.data["last_repre_id"] = str(representation["_id"])
def collect_placeholders(self):
output = []
scene_placeholders = self._collect_scene_placeholders()
for node_name, node in scene_placeholders.items():
plugin_identifier_knob = node.knob("plugin_identifier")
if (
plugin_identifier_knob is None
or plugin_identifier_knob.getValue() != self.identifier
):
continue
placeholder_data = self._parse_placeholder_node_data(node)
# TODO do data validations and maybe updgrades if are invalid
output.append(
LoadPlaceholderItem(node_name, placeholder_data, self)
)
return output
def populate_placeholder(self, placeholder):
self.populate_load_placeholder(placeholder)
def repopulate_placeholder(self, placeholder):
repre_ids = self._get_loaded_repre_ids()
self.populate_load_placeholder(placeholder, repre_ids)
def get_placeholder_options(self, options=None):
return self.get_load_plugin_options(options)
def cleanup_placeholder(self, placeholder, failed):
# deselect all selected nodes
placeholder_node = nuke.toNode(placeholder.scene_identifier)
# getting the latest nodes added
# TODO get from shared populate data!
nodes_init = placeholder.data["nodes_init"]
nodes_loaded = list(set(nuke.allNodes()) - set(nodes_init))
self.log.debug("Loaded nodes: {}".format(nodes_loaded))
if not nodes_loaded:
return
placeholder.data["delete"] = True
nodes_loaded = self._move_to_placeholder_group(
placeholder, nodes_loaded
)
placeholder.data["last_loaded"] = nodes_loaded
refresh_nodes(nodes_loaded)
# positioning of the loaded nodes
min_x, min_y, _, _ = get_extreme_positions(nodes_loaded)
for node in nodes_loaded:
xpos = (node.xpos() - min_x) + placeholder_node.xpos()
ypos = (node.ypos() - min_y) + placeholder_node.ypos()
node.setXYpos(xpos, ypos)
refresh_nodes(nodes_loaded)
# fix the problem of z_order for backdrops
self._fix_z_order(placeholder)
self._imprint_siblings(placeholder)
if placeholder.data["nb_children"] == 0:
# save initial nodes postions and dimensions, update them
# and set inputs and outputs of loaded nodes
self._imprint_inits()
self._update_nodes(placeholder, nuke.allNodes(), nodes_loaded)
self._set_loaded_connections(placeholder)
elif placeholder.data["siblings"]:
# create copies of placeholder siblings for the new loaded nodes,
# set their inputs and outpus and update all nodes positions and
# dimensions and siblings names
siblings = get_nodes_by_names(placeholder.data["siblings"])
refresh_nodes(siblings)
copies = self._create_sib_copies(placeholder)
new_nodes = list(copies.values()) # copies nodes
self._update_nodes(new_nodes, nodes_loaded)
placeholder_node.removeKnob(placeholder_node.knob("siblings"))
new_nodes_name = get_names_from_nodes(new_nodes)
imprint(placeholder_node, {"siblings": new_nodes_name})
self._set_copies_connections(placeholder, copies)
self._update_nodes(
nuke.allNodes(),
new_nodes + nodes_loaded,
20
)
new_siblings = get_names_from_nodes(new_nodes)
placeholder.data["siblings"] = new_siblings
else:
# if the placeholder doesn't have siblings, the loaded
# nodes will be placed in a free space
xpointer, ypointer = find_free_space_to_paste_nodes(
nodes_loaded, direction="bottom", offset=200
)
node = nuke.createNode("NoOp")
reset_selection()
nuke.delete(node)
for node in nodes_loaded:
xpos = (node.xpos() - min_x) + xpointer
ypos = (node.ypos() - min_y) + ypointer
node.setXYpos(xpos, ypos)
placeholder.data["nb_children"] += 1
reset_selection()
# go back to root group
nuke.root().begin()
def _move_to_placeholder_group(self, placeholder, nodes_loaded):
"""
opening the placeholder's group and copying loaded nodes in it.
Returns :
copies (dict) : with copied nodes names and their copies
nodes_loaded (list): the new list of pasted nodes
"""
copies = {}
siblings = get_nodes_by_names(self.data["siblings"])
for node in siblings:
new_node = duplicate_node(node)
groups_name = placeholder.data["group_name"]
reset_selection()
select_nodes(nodes_loaded)
if groups_name:
with node_tempfile() as filepath:
nuke.nodeCopy(filepath)
for node in nuke.selectedNodes():
nuke.delete(node)
group = nuke.toNode(groups_name)
group.begin()
nuke.nodePaste(filepath)
nodes_loaded = nuke.selectedNodes()
return nodes_loaded
x_init = int(new_node.knob("x_init").getValue())
y_init = int(new_node.knob("y_init").getValue())
new_node.setXYpos(x_init, y_init)
if isinstance(new_node, nuke.BackdropNode):
w_init = new_node.knob("w_init").getValue()
h_init = new_node.knob("h_init").getValue()
new_node.knob("bdwidth").setValue(w_init)
new_node.knob("bdheight").setValue(h_init)
refresh_node(node)
if "repre_id" in node.knobs().keys():
node.removeKnob(node.knob("repre_id"))
copies[node.name()] = new_node
return copies
def fix_z_order(self):
def _fix_z_order(self, placeholder):
"""Fix the problem of z_order when a backdrop is loaded."""
nodes_loaded = self.data["last_loaded"]
nodes_loaded = placeholder.data["last_loaded"]
loaded_backdrops = []
bd_orders = set()
for node in nodes_loaded:
@ -287,7 +313,7 @@ class NukePlaceholder(AbstractPlaceholder):
return
sib_orders = set()
for node_name in self.data["siblings"]:
for node_name in placeholder.data["siblings"]:
node = nuke.toNode(node_name)
if isinstance(node, nuke.BackdropNode):
sib_orders.add(node.knob("z_order").getValue())
@ -302,7 +328,56 @@ class NukePlaceholder(AbstractPlaceholder):
backdrop_node.knob("z_order").setValue(
z_order + max_order - min_order + 1)
def update_nodes(self, nodes, considered_nodes, offset_y=None):
def _imprint_siblings(self, placeholder):
"""
- add siblings names to placeholder attributes (nodes loaded with it)
- add Id to the attributes of all the other nodes
"""
loaded_nodes = placeholder.data["last_loaded"]
loaded_nodes_set = set(loaded_nodes)
data = {"repre_id": str(placeholder.data["last_repre_id"])}
for node in loaded_nodes:
node_knobs = node.knobs()
if "builder_type" not in node_knobs:
# save the id of representation for all imported nodes
imprint(node, data)
node.knob("repre_id").setVisible(False)
refresh_node(node)
continue
if (
"is_placeholder" not in node_knobs
or (
"is_placeholder" in node_knobs
and node.knob("is_placeholder").value()
)
):
siblings = list(loaded_nodes_set - {node})
siblings_name = get_names_from_nodes(siblings)
siblings = {"siblings": siblings_name}
imprint(node, siblings)
def _imprint_inits(self):
"""Add initial positions and dimensions to the attributes"""
for node in nuke.allNodes():
refresh_node(node)
imprint(node, {"x_init": node.xpos(), "y_init": node.ypos()})
node.knob("x_init").setVisible(False)
node.knob("y_init").setVisible(False)
width = node.screenWidth()
height = node.screenHeight()
if "bdwidth" in node.knobs():
imprint(node, {"w_init": width, "h_init": height})
node.knob("w_init").setVisible(False)
node.knob("h_init").setVisible(False)
refresh_node(node)
def _update_nodes(
self, placeholder, nodes, considered_nodes, offset_y=None
):
"""Adjust backdrop nodes dimensions and positions.
Considering some nodes sizes.
@ -314,7 +389,7 @@ class NukePlaceholder(AbstractPlaceholder):
offset (int): distance between copies
"""
placeholder_node = self.data["node"]
placeholder_node = nuke.toNode(placeholder.scene_identifier)
min_x, min_y, max_x, max_y = get_extreme_positions(considered_nodes)
@ -330,7 +405,7 @@ class NukePlaceholder(AbstractPlaceholder):
min_x = placeholder_node.xpos()
min_y = placeholder_node.ypos()
else:
siblings = get_nodes_by_names(self.data["siblings"])
siblings = get_nodes_by_names(placeholder.data["siblings"])
minX, _, maxX, _ = get_extreme_positions(siblings)
diff_y = max_y - min_y + 20
diff_x = abs(max_x - min_x - maxX + minX)
@ -369,59 +444,14 @@ class NukePlaceholder(AbstractPlaceholder):
refresh_node(node)
def imprint_inits(self):
"""Add initial positions and dimensions to the attributes"""
for node in nuke.allNodes():
refresh_node(node)
imprint(node, {"x_init": node.xpos(), "y_init": node.ypos()})
node.knob("x_init").setVisible(False)
node.knob("y_init").setVisible(False)
width = node.screenWidth()
height = node.screenHeight()
if "bdwidth" in node.knobs():
imprint(node, {"w_init": width, "h_init": height})
node.knob("w_init").setVisible(False)
node.knob("h_init").setVisible(False)
refresh_node(node)
def imprint_siblings(self):
"""
- add siblings names to placeholder attributes (nodes loaded with it)
- add Id to the attributes of all the other nodes
"""
loaded_nodes = self.data["last_loaded"]
loaded_nodes_set = set(loaded_nodes)
data = {"repre_id": str(self.data["last_repre_id"])}
for node in loaded_nodes:
node_knobs = node.knobs()
if "builder_type" not in node_knobs:
# save the id of representation for all imported nodes
imprint(node, data)
node.knob("repre_id").setVisible(False)
refresh_node(node)
continue
if (
"is_placeholder" not in node_knobs
or (
"is_placeholder" in node_knobs
and node.knob("is_placeholder").value()
)
):
siblings = list(loaded_nodes_set - {node})
siblings_name = get_names_from_nodes(siblings)
siblings = {"siblings": siblings_name}
imprint(node, siblings)
def set_loaded_connections(self):
def _set_loaded_connections(self, placeholder):
"""
set inputs and outputs of loaded nodes"""
placeholder_node = self.data["node"]
input_node, output_node = get_group_io_nodes(self.data["last_loaded"])
placeholder_node = nuke.toNode(placeholder.scene_identifier)
input_node, output_node = get_group_io_nodes(
placeholder.data["last_loaded"]
)
for node in placeholder_node.dependent():
for idx in range(node.inputs()):
if node.input(idx) == placeholder_node:
@ -432,15 +462,45 @@ class NukePlaceholder(AbstractPlaceholder):
if placeholder_node.input(idx) == node:
input_node.setInput(0, node)
def set_copies_connections(self, copies):
def _create_sib_copies(self, placeholder):
""" creating copies of the palce_holder siblings (the ones who were
loaded with it) for the new nodes added
Returns :
copies (dict) : with copied nodes names and their copies
"""
copies = {}
siblings = get_nodes_by_names(placeholder.data["siblings"])
for node in siblings:
new_node = duplicate_node(node)
x_init = int(new_node.knob("x_init").getValue())
y_init = int(new_node.knob("y_init").getValue())
new_node.setXYpos(x_init, y_init)
if isinstance(new_node, nuke.BackdropNode):
w_init = new_node.knob("w_init").getValue()
h_init = new_node.knob("h_init").getValue()
new_node.knob("bdwidth").setValue(w_init)
new_node.knob("bdheight").setValue(h_init)
refresh_node(node)
if "repre_id" in node.knobs().keys():
node.removeKnob(node.knob("repre_id"))
copies[node.name()] = new_node
return copies
def _set_copies_connections(self, placeholder, copies):
"""Set inputs and outputs of the copies.
Args:
copies (dict): Copied nodes by their names.
"""
last_input, last_output = get_group_io_nodes(self.data["last_loaded"])
siblings = get_nodes_by_names(self.data["siblings"])
last_input, last_output = get_group_io_nodes(
placeholder.data["last_loaded"]
)
siblings = get_nodes_by_names(placeholder.data["siblings"])
siblings_input, siblings_output = get_group_io_nodes(siblings)
copy_input = copies[siblings_input.name()]
copy_output = copies[siblings_output.name()]
@ -474,166 +534,45 @@ class NukePlaceholder(AbstractPlaceholder):
siblings_input.setInput(0, copy_output)
def move_to_placeholder_group(self, nodes_loaded):
"""
opening the placeholder's group and copying loaded nodes in it.
Returns :
nodes_loaded (list): the new list of pasted nodes
"""
def build_workfile_template(*args):
builder = NukeTemplateBuilder(registered_host())
builder.build_template()
groups_name = self.data["group_name"]
reset_selection()
select_nodes(nodes_loaded)
if groups_name:
with node_tempfile() as filepath:
nuke.nodeCopy(filepath)
for node in nuke.selectedNodes():
nuke.delete(node)
group = nuke.toNode(groups_name)
group.begin()
nuke.nodePaste(filepath)
nodes_loaded = nuke.selectedNodes()
return nodes_loaded
def clean(self):
# deselect all selected nodes
placeholder_node = self.data["node"]
def update_workfile_template(*args):
builder = NukeTemplateBuilder(registered_host())
builder.rebuild_template()
# getting the latest nodes added
nodes_init = self.data["nodes_init"]
nodes_loaded = list(set(nuke.allNodes()) - set(nodes_init))
self.log.debug("Loaded nodes: {}".format(nodes_loaded))
if not nodes_loaded:
return
self.data["delete"] = True
def create_placeholder(*args):
host = registered_host()
builder = NukeTemplateBuilder(host)
window = WorkfileBuildPlaceholderDialog(host, builder)
window.exec_()
nodes_loaded = self.move_to_placeholder_group(nodes_loaded)
self.data["last_loaded"] = nodes_loaded
refresh_nodes(nodes_loaded)
# positioning of the loaded nodes
min_x, min_y, _, _ = get_extreme_positions(nodes_loaded)
for node in nodes_loaded:
xpos = (node.xpos() - min_x) + placeholder_node.xpos()
ypos = (node.ypos() - min_y) + placeholder_node.ypos()
node.setXYpos(xpos, ypos)
refresh_nodes(nodes_loaded)
def update_placeholder(*args):
host = registered_host()
builder = NukeTemplateBuilder(host)
placeholder_items_by_id = {
placeholder_item.scene_identifier: placeholder_item
for placeholder_item in builder.get_placeholders()
}
placeholder_items = []
for node in nuke.selectedNodes():
node_name = node.fullName()
if node_name in placeholder_items_by_id:
placeholder_items.append(placeholder_items_by_id[node_name])
self.fix_z_order() # fix the problem of z_order for backdrops
self.imprint_siblings()
# TODO show UI at least
if len(placeholder_items) == 0:
raise ValueError("No node selected")
if self.data["nb_children"] == 0:
# save initial nodes postions and dimensions, update them
# and set inputs and outputs of loaded nodes
if len(placeholder_items) > 1:
raise ValueError("Too many selected nodes")
self.imprint_inits()
self.update_nodes(nuke.allNodes(), nodes_loaded)
self.set_loaded_connections()
elif self.data["siblings"]:
# create copies of placeholder siblings for the new loaded nodes,
# set their inputs and outpus and update all nodes positions and
# dimensions and siblings names
siblings = get_nodes_by_names(self.data["siblings"])
refresh_nodes(siblings)
copies = self.create_sib_copies()
new_nodes = list(copies.values()) # copies nodes
self.update_nodes(new_nodes, nodes_loaded)
placeholder_node.removeKnob(placeholder_node.knob("siblings"))
new_nodes_name = get_names_from_nodes(new_nodes)
imprint(placeholder_node, {"siblings": new_nodes_name})
self.set_copies_connections(copies)
self.update_nodes(
nuke.allNodes(),
new_nodes + nodes_loaded,
20
)
new_siblings = get_names_from_nodes(new_nodes)
self.data["siblings"] = new_siblings
else:
# if the placeholder doesn't have siblings, the loaded
# nodes will be placed in a free space
xpointer, ypointer = find_free_space_to_paste_nodes(
nodes_loaded, direction="bottom", offset=200
)
node = nuke.createNode("NoOp")
reset_selection()
nuke.delete(node)
for node in nodes_loaded:
xpos = (node.xpos() - min_x) + xpointer
ypos = (node.ypos() - min_y) + ypointer
node.setXYpos(xpos, ypos)
self.data["nb_children"] += 1
reset_selection()
# go back to root group
nuke.root().begin()
def get_representations(self, current_asset_doc, linked_asset_docs):
project_name = legacy_io.active_project()
builder_type = self.data["builder_type"]
if builder_type == "context_asset":
context_filters = {
"asset": [re.compile(self.data["asset"])],
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representations": [self.data["representation"]],
"family": [self.data["family"]]
}
elif builder_type != "linked_asset":
context_filters = {
"asset": [
current_asset_doc["name"],
re.compile(self.data["asset"])
],
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representation": [self.data["representation"]],
"family": [self.data["family"]]
}
else:
asset_regex = re.compile(self.data["asset"])
linked_asset_names = []
for asset_doc in linked_asset_docs:
asset_name = asset_doc["name"]
if asset_regex.match(asset_name):
linked_asset_names.append(asset_name)
if not linked_asset_names:
return []
context_filters = {
"asset": linked_asset_names,
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representation": [self.data["representation"]],
"family": [self.data["family"]],
}
return list(get_representations(
project_name,
context_filters=context_filters
))
def err_message(self):
return (
"Error while trying to load a representation.\n"
"Either the subset wasn't published or the template is malformed."
"\n\n"
"Builder was looking for:\n{attributes}".format(
attributes="\n".join([
"{}: {}".format(key.title(), value)
for key, value in self.data.items()]
)
)
)
placeholder_item = placeholder_items[0]
window = WorkfileBuildPlaceholderDialog(host, builder)
window.set_update_mode(placeholder_item)
window.exec_()

View file

@ -25,6 +25,8 @@ class CollectReview(pyblish.api.ContextPlugin):
hosts = ["photoshop"]
order = pyblish.api.CollectorOrder + 0.1
publish = True
def process(self, context):
family = "review"
subset = get_subset_name(
@ -45,5 +47,6 @@ class CollectReview(pyblish.api.ContextPlugin):
"family": family,
"families": [],
"representations": [],
"asset": os.environ["AVALON_ASSET"]
"asset": os.environ["AVALON_ASSET"],
"publish": self.publish
})

View file

@ -9,7 +9,7 @@ from openpype.pipeline import (
register_creator_plugin_path,
legacy_io,
)
from openpype.host import HostBase, INewPublisher
from openpype.host import HostBase, IPublishHost
ROOT_DIR = os.path.dirname(os.path.dirname(
@ -19,7 +19,7 @@ PUBLISH_PATH = os.path.join(ROOT_DIR, "plugins", "publish")
CREATE_PATH = os.path.join(ROOT_DIR, "plugins", "create")
class TrayPublisherHost(HostBase, INewPublisher):
class TrayPublisherHost(HostBase, IPublishHost):
name = "traypublisher"
def install(self):

View file

@ -646,9 +646,6 @@ def rename_filepaths_by_frame_start(
filepaths_by_frame, range_start, range_end, new_frame_start
):
"""Change frames in filenames of finished images to new frame start."""
# Skip if source first frame is same as destination first frame
if range_start == new_frame_start:
return
# Calculate frame end
new_frame_end = range_end + (new_frame_start - range_start)
@ -669,14 +666,17 @@ def rename_filepaths_by_frame_start(
source_range = range(range_start, range_end + 1)
output_range = range(new_frame_start, new_frame_end + 1)
# Skip if source first frame is same as destination first frame
new_dst_filepaths = {}
for src_frame, dst_frame in zip(source_range, output_range):
src_filepath = filepaths_by_frame[src_frame]
src_dirpath = os.path.dirname(src_filepath)
src_filepath = os.path.normpath(filepaths_by_frame[src_frame])
dirpath, src_filename = os.path.split(src_filepath)
dst_filename = filename_template.format(frame=dst_frame)
dst_filepath = os.path.join(src_dirpath, dst_filename)
dst_filepath = os.path.join(dirpath, dst_filename)
os.rename(src_filepath, dst_filepath)
if src_filename != dst_filename:
os.rename(src_filepath, dst_filepath)
new_dst_filepaths[dst_frame] = dst_filepath
return new_dst_filepaths

View file

@ -9,6 +9,49 @@ import six
import clique
def get_attributes_keys(attribute_definitions):
"""Collect keys from list of attribute definitions.
Args:
attribute_definitions (List[AbtractAttrDef]): Objects of attribute
definitions.
Returns:
Set[str]: Keys that will be created using passed attribute definitions.
"""
keys = set()
if not attribute_definitions:
return keys
for attribute_def in attribute_definitions:
if not isinstance(attribute_def, UIDef):
keys.add(attribute_def.key)
return keys
def get_default_values(attribute_definitions):
"""Receive default values for attribute definitions.
Args:
attribute_definitions (List[AbtractAttrDef]): Attribute definitions for
which default values should be collected.
Returns:
Dict[str, Any]: Default values for passet attribute definitions.
"""
output = {}
if not attribute_definitions:
return output
for attr_def in attribute_definitions:
# Skip UI definitions
if not isinstance(attr_def, UIDef):
output[attr_def.key] = attr_def.default
return output
class AbstractAttrDefMeta(ABCMeta):
"""Meta class to validate existence of 'key' attribute.

View file

@ -139,7 +139,7 @@ def convert_value_by_type_name(value_type, value, logger=None):
return float(value)
# Vectors will probably have more types
if value_type == "vec2f":
if value_type in ("vec2f", "float2"):
return [float(item) for item in value.split(",")]
# Matrix should be always have square size of element 3x3, 4x4
@ -204,8 +204,8 @@ def convert_value_by_type_name(value_type, value, logger=None):
)
return output
logger.info((
"MISSING IMPLEMENTATION:"
logger.debug((
"Dev note (missing implementation):"
" Unknown attrib type \"{}\". Value: {}"
).format(value_type, value))
return value
@ -263,8 +263,8 @@ def parse_oiio_xml_output(xml_string, logger=None):
# - feel free to add more tags
else:
value = child.text
logger.info((
"MISSING IMPLEMENTATION:"
logger.debug((
"Dev note (missing implementation):"
" Unknown tag \"{}\". Value \"{}\""
).format(tag_name, value))

View file

@ -56,7 +56,7 @@ def convert_value_by_type_name(value_type, value):
return float(value)
# Vectors will probably have more types
if value_type == "vec2f":
if value_type in ("vec2f", "float2"):
return [float(item) for item in value.split(",")]
# Matrix should be always have square size of element 3x3, 4x4
@ -127,7 +127,7 @@ def convert_value_by_type_name(value_type, value):
return output
print((
"MISSING IMPLEMENTATION:"
"Dev note (missing implementation):"
" Unknown attrib type \"{}\". Value: {}"
).format(value_type, value))
return value
@ -183,7 +183,7 @@ def parse_oiio_xml_output(xml_string):
else:
value = child.text
print((
"MISSING IMPLEMENTATION:"
"Dev note (missing implementation):"
" Unknown tag \"{}\". Value \"{}\""
).format(tag_name, value))

View file

@ -7,7 +7,11 @@ from uuid import uuid4
from contextlib import contextmanager
from openpype.client import get_assets
from openpype.host import INewPublisher
from openpype.settings import (
get_system_settings,
get_project_settings
)
from openpype.host import IPublishHost
from openpype.pipeline import legacy_io
from openpype.pipeline.mongodb import (
AvalonMongoDB,
@ -20,11 +24,6 @@ from .creator_plugins import (
discover_creator_plugins,
)
from openpype.api import (
get_system_settings,
get_project_settings
)
UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"])
@ -402,8 +401,12 @@ class CreatedInstance:
self.creator = creator
# Instance members may have actions on them
# TODO implement members logic
self._members = []
# Data that can be used for lifetime of object
self._transient_data = {}
# Create a copy of passed data to avoid changing them on the fly
data = copy.deepcopy(data or {})
# Store original value of passed data
@ -596,6 +599,26 @@ class CreatedInstance:
return self
@property
def transient_data(self):
"""Data stored for lifetime of instance object.
These data are not stored to scene and will be lost on object
deletion.
Can be used to store objects. In some host implementations is not
possible to reference to object in scene with some unique identifier
(e.g. node in Fusion.). In that case it is handy to store the object
here. Should be used that way only if instance data are stored on the
node itself.
Returns:
Dict[str, Any]: Dictionary object where you can store data related
to instance for lifetime of instance object.
"""
return self._transient_data
def changes(self):
"""Calculate and return changes."""
@ -771,7 +794,7 @@ class CreateContext:
"""
missing = set(
INewPublisher.get_missing_publish_methods(host)
IPublishHost.get_missing_publish_methods(host)
)
return missing

View file

@ -81,6 +81,13 @@ class BaseCreator:
# - we may use UI inside processing this attribute should be checked
self.headless = headless
self.apply_settings(project_settings, system_settings)
def apply_settings(self, project_settings, system_settings):
"""Method called on initialization of plugin to apply settings."""
pass
@property
def identifier(self):
"""Identifier of creator (must be unique).

View file

@ -5,6 +5,7 @@ from .utils import (
InvalidRepresentationContext,
get_repres_contexts,
get_contexts_for_repre_docs,
get_subset_contexts,
get_representation_context,
@ -54,6 +55,7 @@ __all__ = (
"InvalidRepresentationContext",
"get_repres_contexts",
"get_contexts_for_repre_docs",
"get_subset_contexts",
"get_representation_context",

View file

@ -37,7 +37,7 @@ log = logging.getLogger(__name__)
ContainersFilterResult = collections.namedtuple(
"ContainersFilterResult",
["latest", "outdated", "not_foud", "invalid"]
["latest", "outdated", "not_found", "invalid"]
)
@ -87,13 +87,20 @@ def get_repres_contexts(representation_ids, dbcon=None):
if not dbcon:
dbcon = legacy_io
contexts = {}
if not representation_ids:
return contexts
return {}
project_name = dbcon.active_project()
repre_docs = get_representations(project_name, representation_ids)
return get_contexts_for_repre_docs(project_name, repre_docs)
def get_contexts_for_repre_docs(project_name, repre_docs):
contexts = {}
if not repre_docs:
return contexts
repre_docs_by_id = {}
version_ids = set()
for repre_doc in repre_docs:
@ -808,7 +815,7 @@ def filter_containers(containers, project_name):
Categories are 'latest', 'outdated', 'invalid' and 'not_found'.
The 'lastest' containers are from last version, 'outdated' are not,
'invalid' are invalid containers (invalid content) and 'not_foud' has
'invalid' are invalid containers (invalid content) and 'not_found' has
some missing entity in database.
Args:

View file

@ -1,528 +0,0 @@
import os
from abc import ABCMeta, abstractmethod
import six
import logging
from functools import reduce
from openpype.client import (
get_asset_by_name,
get_linked_assets,
)
from openpype.settings import get_project_settings
from openpype.lib import (
StringTemplate,
Logger,
filter_profiles,
)
from openpype.pipeline import legacy_io, Anatomy
from openpype.pipeline.load import (
get_loaders_by_name,
get_representation_context,
load_with_repre_context,
)
from .build_template_exceptions import (
TemplateAlreadyImported,
TemplateLoadingFailed,
TemplateProfileNotFound,
TemplateNotFound
)
log = logging.getLogger(__name__)
def update_representations(entities, entity):
if entity['context']['subset'] not in entities:
entities[entity['context']['subset']] = entity
else:
current = entities[entity['context']['subset']]
incomming = entity
entities[entity['context']['subset']] = max(
current, incomming,
key=lambda entity: entity["context"].get("version", -1))
return entities
def parse_loader_args(loader_args):
if not loader_args:
return dict()
try:
parsed_args = eval(loader_args)
if not isinstance(parsed_args, dict):
return dict()
else:
return parsed_args
except Exception as err:
print(
"Error while parsing loader arguments '{}'.\n{}: {}\n\n"
"Continuing with default arguments. . .".format(
loader_args,
err.__class__.__name__,
err))
return dict()
@six.add_metaclass(ABCMeta)
class AbstractTemplateLoader:
"""
Abstraction of Template Loader.
Properties:
template_path : property to get current template path
Methods:
import_template : Abstract Method. Used to load template,
depending on current host
get_template_nodes : Abstract Method. Used to query nodes acting
as placeholders. Depending on current host
"""
_log = None
def __init__(self, placeholder_class):
# TODO template loader should expect host as and argument
# - host have all responsibility for most of code (also provide
# placeholder class)
# - also have responsibility for current context
# - this won't work in DCCs where multiple workfiles with
# different contexts can be opened at single time
# - template loader should have ability to change context
project_name = legacy_io.active_project()
asset_name = legacy_io.Session["AVALON_ASSET"]
self.loaders_by_name = get_loaders_by_name()
self.current_asset = asset_name
self.project_name = project_name
self.host_name = legacy_io.Session["AVALON_APP"]
self.task_name = legacy_io.Session["AVALON_TASK"]
self.placeholder_class = placeholder_class
self.current_asset_doc = get_asset_by_name(project_name, asset_name)
self.task_type = (
self.current_asset_doc
.get("data", {})
.get("tasks", {})
.get(self.task_name, {})
.get("type")
)
self.log.info(
"BUILDING ASSET FROM TEMPLATE :\n"
"Starting templated build for {asset} in {project}\n\n"
"Asset : {asset}\n"
"Task : {task_name} ({task_type})\n"
"Host : {host}\n"
"Project : {project}\n".format(
asset=self.current_asset,
host=self.host_name,
project=self.project_name,
task_name=self.task_name,
task_type=self.task_type
))
# Skip if there is no loader
if not self.loaders_by_name:
self.log.warning(
"There is no registered loaders. No assets will be loaded")
return
@property
def log(self):
if self._log is None:
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
def template_already_imported(self, err_msg):
"""In case template was already loaded.
Raise the error as a default action.
Override this method in your template loader implementation
to manage this case."""
self.log.error("{}: {}".format(
err_msg.__class__.__name__,
err_msg))
raise TemplateAlreadyImported(err_msg)
def template_loading_failed(self, err_msg):
"""In case template loading failed
Raise the error as a default action.
Override this method in your template loader implementation
to manage this case.
"""
self.log.error("{}: {}".format(
err_msg.__class__.__name__,
err_msg))
raise TemplateLoadingFailed(err_msg)
@property
def template_path(self):
"""
Property returning template path. Avoiding setter.
Getting template path from open pype settings based on current avalon
session and solving the path variables if needed.
Returns:
str: Solved template path
Raises:
TemplateProfileNotFound: No profile found from settings for
current avalon session
KeyError: Could not solve path because a key does not exists
in avalon context
TemplateNotFound: Solved path does not exists on current filesystem
"""
project_name = self.project_name
host_name = self.host_name
task_name = self.task_name
task_type = self.task_type
anatomy = Anatomy(project_name)
project_settings = get_project_settings(project_name)
build_info = project_settings[host_name]["templated_workfile_build"]
profile = filter_profiles(
build_info["profiles"],
{
"task_types": task_type,
"task_names": task_name
}
)
if not profile:
raise TemplateProfileNotFound(
"No matching profile found for task '{}' of type '{}' "
"with host '{}'".format(task_name, task_type, host_name)
)
path = profile["path"]
if not path:
raise TemplateLoadingFailed(
"Template path is not set.\n"
"Path need to be set in {}\\Template Workfile Build "
"Settings\\Profiles".format(host_name.title()))
# Try fill path with environments and anatomy roots
fill_data = {
key: value
for key, value in os.environ.items()
}
fill_data["root"] = anatomy.roots
result = StringTemplate.format_template(path, fill_data)
if result.solved:
path = result.normalized()
if path and os.path.exists(path):
self.log.info("Found template at: '{}'".format(path))
return path
solved_path = None
while True:
try:
solved_path = anatomy.path_remapper(path)
except KeyError as missing_key:
raise KeyError(
"Could not solve key '{}' in template path '{}'".format(
missing_key, path))
if solved_path is None:
solved_path = path
if solved_path == path:
break
path = solved_path
solved_path = os.path.normpath(solved_path)
if not os.path.exists(solved_path):
raise TemplateNotFound(
"Template found in openPype settings for task '{}' with host "
"'{}' does not exists. (Not found : {})".format(
task_name, host_name, solved_path))
self.log.info("Found template at: '{}'".format(solved_path))
return solved_path
def populate_template(self, ignored_ids=None):
"""
Use template placeholders to load assets and parent them in hierarchy
Arguments :
ignored_ids :
Returns:
None
"""
loaders_by_name = self.loaders_by_name
current_asset_doc = self.current_asset_doc
linked_assets = get_linked_assets(current_asset_doc)
ignored_ids = ignored_ids or []
placeholders = self.get_placeholders()
self.log.debug("Placeholders found in template: {}".format(
[placeholder.name for placeholder in placeholders]
))
for placeholder in placeholders:
self.log.debug("Start to processing placeholder {}".format(
placeholder.name
))
placeholder_representations = self.get_placeholder_representations(
placeholder,
current_asset_doc,
linked_assets
)
if not placeholder_representations:
self.log.info(
"There's no representation for this placeholder: "
"{}".format(placeholder.name)
)
continue
for representation in placeholder_representations:
self.preload(placeholder, loaders_by_name, representation)
if self.load_data_is_incorrect(
placeholder,
representation,
ignored_ids):
continue
self.log.info(
"Loading {}_{} with loader {}\n"
"Loader arguments used : {}".format(
representation['context']['asset'],
representation['context']['subset'],
placeholder.loader_name,
placeholder.loader_args))
try:
container = self.load(
placeholder, loaders_by_name, representation)
except Exception:
self.load_failed(placeholder, representation)
else:
self.load_succeed(placeholder, container)
finally:
self.postload(placeholder)
def get_placeholder_representations(
self, placeholder, current_asset_doc, linked_asset_docs
):
placeholder_representations = placeholder.get_representations(
current_asset_doc,
linked_asset_docs
)
for repre_doc in reduce(
update_representations,
placeholder_representations,
dict()
).values():
yield repre_doc
def load_data_is_incorrect(
self, placeholder, last_representation, ignored_ids):
if not last_representation:
self.log.warning(placeholder.err_message())
return True
if (str(last_representation['_id']) in ignored_ids):
print("Ignoring : ", last_representation['_id'])
return True
return False
def preload(self, placeholder, loaders_by_name, last_representation):
pass
def load(self, placeholder, loaders_by_name, last_representation):
repre = get_representation_context(last_representation)
return load_with_repre_context(
loaders_by_name[placeholder.loader_name],
repre,
options=parse_loader_args(placeholder.loader_args))
def load_succeed(self, placeholder, container):
placeholder.parent_in_hierarchy(container)
def load_failed(self, placeholder, last_representation):
self.log.warning(
"Got error trying to load {}:{} with {}".format(
last_representation['context']['asset'],
last_representation['context']['subset'],
placeholder.loader_name
),
exc_info=True
)
def postload(self, placeholder):
placeholder.clean()
def update_missing_containers(self):
loaded_containers_ids = self.get_loaded_containers_by_id()
self.populate_template(ignored_ids=loaded_containers_ids)
def get_placeholders(self):
placeholders = map(self.placeholder_class, self.get_template_nodes())
valid_placeholders = filter(
lambda i: i.is_valid,
placeholders
)
sorted_placeholders = list(sorted(
valid_placeholders,
key=lambda i: i.order
))
return sorted_placeholders
@abstractmethod
def get_loaded_containers_by_id(self):
"""
Collect already loaded containers for updating scene
Return:
dict (string, node): A dictionnary id as key
and containers as value
"""
pass
@abstractmethod
def import_template(self, template_path):
"""
Import template in current host
Args:
template_path (str): fullpath to current task and
host's template file
Return:
None
"""
pass
@abstractmethod
def get_template_nodes(self):
"""
Returning a list of nodes acting as host placeholders for
templating. The data representation is by user.
AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes
Args :
None
Returns:
list(AnyNode): Solved template path
"""
pass
@six.add_metaclass(ABCMeta)
class AbstractPlaceholder:
"""Abstraction of placeholders logic.
Properties:
required_keys: A list of mandatory keys to decribe placeholder
and assets to load.
optional_keys: A list of optional keys to decribe
placeholder and assets to load
loader_name: Name of linked loader to use while loading assets
Args:
identifier (str): Placeholder identifier. Should be possible to be
used as identifier in "a scene" (e.g. unique node name).
"""
required_keys = {
"builder_type",
"family",
"representation",
"order",
"loader",
"loader_args"
}
optional_keys = {}
def __init__(self, identifier):
self._log = None
self._name = identifier
self.get_data(identifier)
@property
def log(self):
if self._log is None:
self._log = Logger.get_logger(repr(self))
return self._log
def __repr__(self):
return "< {} {} >".format(self.__class__.__name__, self.name)
@property
def name(self):
return self._name
@property
def loader_args(self):
return self.data["loader_args"]
@property
def builder_type(self):
return self.data["builder_type"]
@property
def order(self):
return self.data["order"]
@property
def loader_name(self):
"""Return placeholder loader name.
Returns:
str: Loader name that will be used to load placeholder
representations.
"""
return self.data["loader"]
@property
def is_valid(self):
"""Test validity of placeholder.
i.e.: every required key exists in placeholder data
Returns:
bool: True if every key is in data
"""
if set(self.required_keys).issubset(self.data.keys()):
self.log.debug("Valid placeholder : {}".format(self.name))
return True
self.log.info("Placeholder is not valid : {}".format(self.name))
return False
@abstractmethod
def parent_in_hierarchy(self, container):
"""Place loaded container in correct hierarchy given by placeholder
Args:
container (Dict[str, Any]): Loaded container created by loader.
"""
pass
@abstractmethod
def clean(self):
"""Clean placeholder from hierarchy after loading assets."""
pass
@abstractmethod
def get_representations(self, current_asset_doc, linked_asset_docs):
"""Query representations based on placeholder data.
Args:
current_asset_doc (Dict[str, Any]): Document of current
context asset.
linked_asset_docs (List[Dict[str, Any]]): Documents of assets
linked to current context asset.
Returns:
Iterable[Dict[str, Any]]: Representations that are matching
placeholder filters.
"""
pass
@abstractmethod
def get_data(self, identifier):
"""Collect information about placeholder by identifier.
Args:
identifier (str): A unique placeholder identifier defined by
implementation.
"""
pass

View file

@ -1,72 +0,0 @@
import os
from importlib import import_module
from openpype.lib import classes_from_module
from openpype.host import HostBase
from openpype.pipeline import registered_host
from .abstract_template_loader import (
AbstractPlaceholder,
AbstractTemplateLoader)
from .build_template_exceptions import (
TemplateLoadingFailed,
TemplateAlreadyImported,
MissingHostTemplateModule,
MissingTemplatePlaceholderClass,
MissingTemplateLoaderClass
)
_module_path_format = 'openpype.hosts.{host}.api.template_loader'
def build_workfile_template(*args):
template_loader = build_template_loader()
try:
template_loader.import_template(template_loader.template_path)
except TemplateAlreadyImported as err:
template_loader.template_already_imported(err)
except TemplateLoadingFailed as err:
template_loader.template_loading_failed(err)
else:
template_loader.populate_template()
def update_workfile_template(*args):
template_loader = build_template_loader()
template_loader.update_missing_containers()
def build_template_loader():
# TODO refactor to use advantage of 'HostBase' and don't import dynamically
# - hosts should have methods that gives option to return builders
host = registered_host()
if isinstance(host, HostBase):
host_name = host.name
else:
host_name = os.environ.get("AVALON_APP")
if not host_name:
host_name = host.__name__.split(".")[-2]
module_path = _module_path_format.format(host=host_name)
module = import_module(module_path)
if not module:
raise MissingHostTemplateModule(
"No template loader found for host {}".format(host_name))
template_loader_class = classes_from_module(
AbstractTemplateLoader,
module
)
template_placeholder_class = classes_from_module(
AbstractPlaceholder,
module
)
if not template_loader_class:
raise MissingTemplateLoaderClass()
template_loader_class = template_loader_class[0]
if not template_placeholder_class:
raise MissingTemplatePlaceholderClass()
template_placeholder_class = template_placeholder_class[0]
return template_loader_class(template_placeholder_class)

View file

@ -1,35 +0,0 @@
class MissingHostTemplateModule(Exception):
"""Error raised when expected module does not exists"""
pass
class MissingTemplatePlaceholderClass(Exception):
"""Error raised when module doesn't implement a placeholder class"""
pass
class MissingTemplateLoaderClass(Exception):
"""Error raised when module doesn't implement a template loader class"""
pass
class TemplateNotFound(Exception):
"""Exception raised when template does not exist."""
pass
class TemplateProfileNotFound(Exception):
"""Exception raised when current profile
doesn't match any template profile"""
pass
class TemplateAlreadyImported(Exception):
"""Error raised when Template was already imported by host for
this session"""
pass
class TemplateLoadingFailed(Exception):
"""Error raised whend Template loader was unable to load the template"""
pass

View file

@ -1,3 +1,14 @@
"""Workfile build based on settings.
Workfile builder will do stuff based on project settings. Advantage is that
it need only access to settings. Disadvantage is that it is hard to focus
build per context and being explicit about loaded content.
For more explicit workfile build is recommended 'AbstractTemplateBuilder'
from '~/openpype/pipeline/workfile/workfile_template_builder'. Which gives
more abilities to define how build happens but require more code to achive it.
"""
import os
import re
import collections

File diff suppressed because it is too large Load diff

View file

@ -25,7 +25,9 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
for created_instance in create_context.instances:
instance_data = created_instance.data_to_store()
if instance_data["active"]:
self.create_instance(context, instance_data)
self.create_instance(
context, instance_data, created_instance.transient_data
)
# Update global data to context
context.data.update(create_context.context_data_to_store())
@ -37,7 +39,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
legacy_io.Session[key] = value
os.environ[key] = value
def create_instance(self, context, in_data):
def create_instance(self, context, in_data, transient_data):
subset = in_data["subset"]
# If instance data already contain families then use it
instance_families = in_data.get("families") or []
@ -56,5 +58,8 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin):
for key, value in in_data.items():
if key not in instance.data:
instance.data[key] = value
instance.data["transientData"] = transient_data
self.log.info("collected instance: {}".format(instance.data))
self.log.info("parsing data: {}".format(in_data))

View file

@ -10,6 +10,7 @@ import opentimelineio as otio
import pyblish.api
from pprint import pformat
from openpype.pipeline.editorial import (
get_media_range_with_retimes,
otio_range_to_frame_range,
otio_range_with_handles
)
@ -29,7 +30,7 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
# get basic variables
otio_clip = instance.data["otioClip"]
workfile_start = instance.data["workfileFrameStart"]
workfile_source_duration = instance.data.get("notRetimedFramerange")
workfile_source_duration = instance.data.get("shotDurationFromSource")
# get ranges
otio_tl_range = otio_clip.range_in_parent()
@ -57,8 +58,15 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin):
# in case of retimed clip and frame range should not be retimed
if workfile_source_duration:
frame_end = frame_start + otio.opentime.to_frames(
otio_src_range.duration, otio_src_range.duration.rate) - 1
# get available range trimmed with processed retimes
retimed_attributes = get_media_range_with_retimes(
otio_clip, 0, 0)
self.log.debug(
">> retimed_attributes: {}".format(retimed_attributes))
media_in = int(retimed_attributes["mediaIn"])
media_out = int(retimed_attributes["mediaOut"])
frame_end = frame_start + (media_out - media_in) + 1
self.log.debug(frame_end)
data = {
"frameStart": frame_start,

View file

@ -218,7 +218,7 @@ class PypeCommands:
RuntimeError: When there is no path to process.
"""
from openpype.hosts.webpublisher.cli_functions import (
from openpype.hosts.webpublisher.publish_functions import (
cli_publish
)

View file

@ -17,7 +17,9 @@
"workfileFrameStart": 1001,
"handleStart": 5,
"handleEnd": 5,
"includeHandles": false
"includeHandles": false,
"retimedHandles": true,
"retimedFramerange": true
}
},
"publish": {

View file

@ -15,6 +15,9 @@
"CollectInstances": {
"flatten_subset_template": ""
},
"CollectReview": {
"publish": true
},
"CollectVersion": {
"enabled": false
},

View file

@ -128,6 +128,16 @@
"type": "boolean",
"key": "includeHandles",
"label": "Enable handles including"
},
{
"type": "boolean",
"key": "retimedHandles",
"label": "Enable retimed handles"
},
{
"type": "boolean",
"key": "retimedFramerange",
"label": "Enable retimed shot frameranges"
}
]
}

View file

@ -134,6 +134,18 @@
{
"type": "dict",
"collapsible": true,
"key": "CollectReview",
"label": "Collect Review",
"children": [
{
"type": "boolean",
"key": "publish",
"label": "Active"
}
]
},
{
"type": "dict",
"key": "CollectVersion",
"label": "Collect Version",
"children": [

View file

@ -20,7 +20,7 @@
"color": {
"font": "#D3D8DE",
"font-hover": "#F0F2F5",
"font-disabled": "#99A3B2",
"font-disabled": "#5b6779",
"font-view-selection": "#ffffff",
"font-view-hover": "#F0F2F5",

View file

@ -16,6 +16,7 @@ from openpype.tools.utils import (
BaseClickableFrame,
set_style_property,
)
from openpype.style import get_objected_colors
from openpype.pipeline.create import (
SUBSET_NAME_ALLOWED_SYMBOLS,
TaskNotSetError,
@ -125,28 +126,21 @@ class PublishIconBtn(IconButton):
def __init__(self, pixmap_path, *args, **kwargs):
super(PublishIconBtn, self).__init__(*args, **kwargs)
loaded_image = QtGui.QImage(pixmap_path)
colors = get_objected_colors()
icon = self.generate_icon(
pixmap_path,
enabled_color=colors["font"].get_qcolor(),
disabled_color=colors["font-disabled"].get_qcolor())
self.setIcon(icon)
pixmap = self.paint_image_with_color(loaded_image, QtCore.Qt.white)
self._base_image = loaded_image
self._enabled_icon = QtGui.QIcon(pixmap)
self._disabled_icon = None
self.setIcon(self._enabled_icon)
def get_enabled_icon(self):
"""Enabled icon."""
return self._enabled_icon
def get_disabled_icon(self):
"""Disabled icon."""
if self._disabled_icon is None:
pixmap = self.paint_image_with_color(
self._base_image, QtCore.Qt.gray
)
self._disabled_icon = QtGui.QIcon(pixmap)
return self._disabled_icon
def generate_icon(self, pixmap_path, enabled_color, disabled_color):
icon = QtGui.QIcon()
image = QtGui.QImage(pixmap_path)
enabled_pixmap = self.paint_image_with_color(image, enabled_color)
icon.addPixmap(enabled_pixmap, icon.Normal)
disabled_pixmap = self.paint_image_with_color(image, disabled_color)
icon.addPixmap(disabled_pixmap, icon.Disabled)
return icon
@staticmethod
def paint_image_with_color(image, color):
@ -187,13 +181,6 @@ class PublishIconBtn(IconButton):
return pixmap
def setEnabled(self, enabled):
super(PublishIconBtn, self).setEnabled(enabled)
if self.isEnabled():
self.setIcon(self.get_enabled_icon())
else:
self.setIcon(self.get_disabled_icon())
class ResetBtn(PublishIconBtn):
"""Publish reset button."""

View file

@ -0,0 +1,5 @@
from .window import WorkfileBuildPlaceholderDialog
__all__ = (
"WorkfileBuildPlaceholderDialog",
)

View file

@ -0,0 +1,242 @@
from Qt import QtWidgets
from openpype import style
from openpype.lib import Logger
from openpype.pipeline import legacy_io
from openpype.widgets.attribute_defs import AttributeDefinitionsWidget
class WorkfileBuildPlaceholderDialog(QtWidgets.QDialog):
def __init__(self, host, builder, parent=None):
super(WorkfileBuildPlaceholderDialog, self).__init__(parent)
self.setWindowTitle("Workfile Placeholder Manager")
self._log = None
self._first_show = True
self._first_refreshed = False
self._builder = builder
self._host = host
# Mode can be 0 (create) or 1 (update)
# TODO write it a little bit better
self._mode = 0
self._update_item = None
self._last_selected_plugin = None
host_name = getattr(self._host, "name", None)
if not host_name:
host_name = legacy_io.Session.get("AVALON_APP") or "NA"
self._host_name = host_name
plugins_combo = QtWidgets.QComboBox(self)
content_widget = QtWidgets.QWidget(self)
content_layout = QtWidgets.QVBoxLayout(content_widget)
content_layout.setContentsMargins(0, 0, 0, 0)
btns_widget = QtWidgets.QWidget(self)
create_btn = QtWidgets.QPushButton("Create", btns_widget)
save_btn = QtWidgets.QPushButton("Save", btns_widget)
close_btn = QtWidgets.QPushButton("Close", btns_widget)
create_btn.setVisible(False)
save_btn.setVisible(False)
btns_layout = QtWidgets.QHBoxLayout(btns_widget)
btns_layout.addStretch(1)
btns_layout.addWidget(create_btn, 0)
btns_layout.addWidget(save_btn, 0)
btns_layout.addWidget(close_btn, 0)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addWidget(plugins_combo, 0)
main_layout.addWidget(content_widget, 1)
main_layout.addWidget(btns_widget, 0)
create_btn.clicked.connect(self._on_create_click)
save_btn.clicked.connect(self._on_save_click)
close_btn.clicked.connect(self._on_close_click)
plugins_combo.currentIndexChanged.connect(self._on_plugin_change)
self._attr_defs_widget = None
self._plugins_combo = plugins_combo
self._content_widget = content_widget
self._content_layout = content_layout
self._create_btn = create_btn
self._save_btn = save_btn
self._close_btn = close_btn
@property
def log(self):
if self._log is None:
self._log = Logger.get_logger(self.__class__.__name__)
return self._log
def _clear_content_widget(self):
while self._content_layout.count() > 0:
item = self._content_layout.takeAt(0)
widget = item.widget()
if widget:
widget.setVisible(False)
widget.deleteLater()
def _add_message_to_content(self, message):
msg_label = QtWidgets.QLabel(message, self._content_widget)
self._content_layout.addWidget(msg_label, 0)
self._content_layout.addStretch(1)
def refresh(self):
self._first_refreshed = True
self._clear_content_widget()
if not self._builder:
self._add_message_to_content((
"Host \"{}\" does not have implemented logic"
" for template workfile build."
).format(self._host_name))
self._update_ui_visibility()
return
placeholder_plugins = self._builder.placeholder_plugins
if self._mode == 1:
self._last_selected_plugin
plugin = self._builder.placeholder_plugins.get(
self._last_selected_plugin
)
self._create_option_widgets(
plugin, self._update_item.to_dict()
)
self._update_ui_visibility()
return
if not placeholder_plugins:
self._add_message_to_content((
"Host \"{}\" does not have implemented plugins"
" for template workfile build."
).format(self._host_name))
self._update_ui_visibility()
return
last_selected_plugin = self._last_selected_plugin
self._last_selected_plugin = None
self._plugins_combo.clear()
for identifier, plugin in placeholder_plugins.items():
label = plugin.label or identifier
self._plugins_combo.addItem(label, identifier)
index = self._plugins_combo.findData(last_selected_plugin)
if index < 0:
index = 0
self._plugins_combo.setCurrentIndex(index)
self._on_plugin_change()
self._update_ui_visibility()
def set_create_mode(self):
if self._mode == 0:
return
self._mode = 0
self._update_item = None
self.refresh()
def set_update_mode(self, update_item):
if self._mode == 1:
return
self._mode = 1
self._update_item = update_item
if update_item:
self._last_selected_plugin = update_item.plugin.identifier
self.refresh()
return
self._clear_content_widget()
self._add_message_to_content((
"Nothing to update."
" (You maybe don't have selected placeholder.)"
))
self._update_ui_visibility()
def _create_option_widgets(self, plugin, options=None):
self._clear_content_widget()
attr_defs = plugin.get_placeholder_options(options)
widget = AttributeDefinitionsWidget(attr_defs, self._content_widget)
self._content_layout.addWidget(widget, 0)
self._content_layout.addStretch(1)
self._attr_defs_widget = widget
self._last_selected_plugin = plugin.identifier
def _update_ui_visibility(self):
create_mode = self._mode == 0
self._plugins_combo.setVisible(create_mode)
if not self._builder:
self._save_btn.setVisible(False)
self._create_btn.setVisible(False)
return
save_enabled = not create_mode
if save_enabled:
save_enabled = self._update_item is not None
self._save_btn.setVisible(save_enabled)
self._create_btn.setVisible(create_mode)
def _on_plugin_change(self):
index = self._plugins_combo.currentIndex()
plugin_identifier = self._plugins_combo.itemData(index)
if plugin_identifier == self._last_selected_plugin:
return
plugin = self._builder.placeholder_plugins.get(plugin_identifier)
self._create_option_widgets(plugin)
def _on_save_click(self):
options = self._attr_defs_widget.current_value()
plugin = self._builder.placeholder_plugins.get(
self._last_selected_plugin
)
# TODO much better error handling
try:
plugin.update_placeholder(self._update_item, options)
self.accept()
except Exception:
self.log.warning("Something went wrong", exc_info=True)
dialog = QtWidgets.QMessageBox(self)
dialog.setWindowTitle("Something went wrong")
dialog.setText("Something went wrong")
dialog.exec_()
def _on_create_click(self):
options = self._attr_defs_widget.current_value()
plugin = self._builder.placeholder_plugins.get(
self._last_selected_plugin
)
# TODO much better error handling
try:
plugin.create_placeholder(options)
self.accept()
except Exception:
self.log.warning("Something went wrong", exc_info=True)
dialog = QtWidgets.QMessageBox(self)
dialog.setWindowTitle("Something went wrong")
dialog.setText("Something went wrong")
dialog.exec_()
def _on_close_click(self):
self.reject()
def showEvent(self, event):
super(WorkfileBuildPlaceholderDialog, self).showEvent(event)
if not self._first_refreshed:
self.refresh()
if self._first_show:
self._first_show = False
self.setStyleSheet(style.load_stylesheet())
self.resize(390, 450)

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.14.3-nightly.2"
__version__ = "3.14.3-nightly.4"

View file

@ -108,10 +108,12 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
row = 0
for attr_def in attr_defs:
if attr_def.key in self._current_keys:
raise KeyError("Duplicated key \"{}\"".format(attr_def.key))
if not isinstance(attr_def, UIDef):
if attr_def.key in self._current_keys:
raise KeyError(
"Duplicated key \"{}\"".format(attr_def.key))
self._current_keys.add(attr_def.key)
self._current_keys.add(attr_def.key)
widget = create_widget_for_attr_def(attr_def, self)
expand_cols = 2

View file

@ -111,14 +111,14 @@ class NiceCheckbox(QtWidgets.QFrame):
return QtCore.QSize(width, height)
def get_width_hint_by_height(self, height):
return (
height / self._base_size.height()
) * self._base_size.width()
return int((
float(height) / self._base_size.height()
) * self._base_size.width())
def get_height_hint_by_width(self, width):
return (
width / self._base_size.width()
) * self._base_size.height()
return int((
float(width) / self._base_size.width()
) * self._base_size.height())
def setFixedHeight(self, *args, **kwargs):
self._fixed_height_set = True
@ -321,7 +321,7 @@ class NiceCheckbox(QtWidgets.QFrame):
bg_color = self.unchecked_bg_color
else:
offset_ratio = self._current_step / self._steps
offset_ratio = float(self._current_step) / self._steps
# Animation bg
bg_color = self.steped_color(
self.checked_bg_color,
@ -332,7 +332,8 @@ class NiceCheckbox(QtWidgets.QFrame):
margins_ratio = self._checker_margins_divider
if margins_ratio > 0:
size_without_margins = int(
(frame_rect.height() / margins_ratio) * (margins_ratio - 2)
(float(frame_rect.height()) / margins_ratio)
* (margins_ratio - 2)
)
size_without_margins -= size_without_margins % 2
margin_size_c = ceil(
@ -434,21 +435,21 @@ class NiceCheckbox(QtWidgets.QFrame):
def _get_enabled_icon_path(
self, painter, checker_rect, step=None, half_steps=None
):
fifteenth = checker_rect.height() / 15
fifteenth = float(checker_rect.height()) / 15
# Left point
p1 = QtCore.QPoint(
checker_rect.x() + (5 * fifteenth),
checker_rect.y() + (9 * fifteenth)
int(checker_rect.x() + (5 * fifteenth)),
int(checker_rect.y() + (9 * fifteenth))
)
# Middle bottom point
p2 = QtCore.QPoint(
checker_rect.center().x(),
checker_rect.y() + (11 * fifteenth)
int(checker_rect.y() + (11 * fifteenth))
)
# Top right point
p3 = QtCore.QPoint(
checker_rect.x() + (10 * fifteenth),
checker_rect.y() + (5 * fifteenth)
int(checker_rect.x() + (10 * fifteenth)),
int(checker_rect.y() + (5 * fifteenth))
)
if step is not None:
multiplier = (half_steps - step)
@ -458,16 +459,16 @@ class NiceCheckbox(QtWidgets.QFrame):
p3c = p3 - checker_rect.center()
p1o = QtCore.QPoint(
(p1c.x() / half_steps) * multiplier,
(p1c.y() / half_steps) * multiplier
int((float(p1c.x()) / half_steps) * multiplier),
int((float(p1c.y()) / half_steps) * multiplier)
)
p2o = QtCore.QPoint(
(p2c.x() / half_steps) * multiplier,
(p2c.y() / half_steps) * multiplier
int((float(p2c.x()) / half_steps) * multiplier),
int((float(p2c.y()) / half_steps) * multiplier)
)
p3o = QtCore.QPoint(
(p3c.x() / half_steps) * multiplier,
(p3c.y() / half_steps) * multiplier
int((float(p3c.x()) / half_steps) * multiplier),
int((float(p3c.y()) / half_steps) * multiplier)
)
p1 -= p1o
@ -484,11 +485,12 @@ class NiceCheckbox(QtWidgets.QFrame):
self, painter, checker_rect, step=None, half_steps=None
):
center_point = QtCore.QPointF(
checker_rect.width() / 2, checker_rect.height() / 2
float(checker_rect.width()) / 2,
float(checker_rect.height()) / 2
)
offset = (
offset = float((
(center_point + QtCore.QPointF(0, 0)) / 2
).x() / 4 * 5
).x()) / 4 * 5
if step is not None:
diff = center_point.x() - offset
diff_offset = (diff / half_steps) * (half_steps - step)

View file

@ -10,7 +10,7 @@ import glob
import platform
from tests.lib.db_handler import DBHandler
from tests.lib.file_handler import RemoteFileHandler
from distribution.file_handler import RemoteFileHandler
class BaseTest: