Merge pull request #4270 from ynput/feature/OP-4406_color-v3-global-color-plugins

This commit is contained in:
Jakub Ježek 2023-01-10 10:46:33 +01:00 committed by GitHub
commit 36b3ccdd54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 2127 additions and 841 deletions

View file

@ -1,4 +1,3 @@
import enlighten
import os
import re
import urllib
@ -252,6 +251,11 @@ class RemoteFileHandler:
if key.startswith('download_warning'):
return value
# handle antivirus warning for big zips
found = re.search("(confirm=)([^&.+])", response.text)
if found:
return found.groups()[1]
return None
@staticmethod
@ -259,15 +263,9 @@ class RemoteFileHandler:
response_gen, destination,
):
with open(destination, "wb") as f:
pbar = enlighten.Counter(
total=None, desc="Save content", units="%", color="green")
progress = 0
for chunk in response_gen:
if chunk: # filter out keep-alive new chunks
f.write(chunk)
progress += len(chunk)
pbar.close()
@staticmethod
def _quota_exceeded(first_chunk):

View file

@ -7,8 +7,7 @@ import nuke
from openpype.pipeline import publish
class NukeRenderLocal(publish.Extractor):
# TODO: rewrite docstring to nuke
class NukeRenderLocal(publish.ExtractorColormanaged):
"""Render the current Nuke composition locally.
Extract the result of savers by starting a comp render
@ -22,6 +21,10 @@ class NukeRenderLocal(publish.Extractor):
families = ["render.local", "prerender.local", "still.local"]
def process(self, instance):
# get colorspace settings data
config_data, file_rules = self.get_colorspace_settings(
instance.context)
families = instance.data["families"]
node = None
@ -67,6 +70,7 @@ class NukeRenderLocal(publish.Extractor):
)
ext = node["file_type"].value()
colorspace = node["colorspace"].value()
if "representations" not in instance.data:
instance.data["representations"] = []
@ -90,6 +94,14 @@ class NukeRenderLocal(publish.Extractor):
'files': filenames,
"stagingDir": out_dir
}
# inject colorspace data
self.set_representation_colorspace(
repre, instance.context,
config_data, file_rules,
colorspace=colorspace
)
instance.data["representations"].append(repre)
self.log.info("Extracted instance '{0}' to: {1}".format(

View file

@ -0,0 +1,423 @@
from copy import deepcopy
import re
import os
import sys
import json
import platform
import contextlib
import tempfile
from openpype import PACKAGE_DIR
from openpype.settings import get_project_settings
from openpype.lib import (
StringTemplate,
run_openpype_process,
Logger
)
from openpype.pipeline import Anatomy
log = Logger.get_logger(__name__)
@contextlib.contextmanager
def _make_temp_json_file():
"""Wrapping function for json temp file
"""
try:
# Store dumped json to temporary file
temporary_json_file = tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False
)
temporary_json_file.close()
temporary_json_filepath = temporary_json_file.name.replace(
"\\", "/"
)
yield temporary_json_filepath
except IOError as _error:
raise IOError(
"Not able to create temp json file: {}".format(
_error
)
)
finally:
# Remove the temporary json
os.remove(temporary_json_filepath)
def get_ocio_config_script_path():
"""Get path to ocio wrapper script
Returns:
str: path string
"""
return os.path.normpath(
os.path.join(
PACKAGE_DIR,
"scripts",
"ocio_wrapper.py"
)
)
def get_imageio_colorspace_from_filepath(
path, host_name, project_name,
config_data=None, file_rules=None,
project_settings=None,
validate=True
):
"""Get colorspace name from filepath
ImageIO Settings file rules are tested for matching rule.
Args:
path (str): path string, file rule pattern is tested on it
host_name (str): host name
project_name (str): project name
config_data (dict, optional): config path and template in dict.
Defaults to None.
file_rules (dict, optional): file rule data from settings.
Defaults to None.
project_settings (dict, optional): project settings. Defaults to None.
validate (bool, optional): should resulting colorspace be validated
with config file? Defaults to True.
Returns:
str: name of colorspace
"""
if not any([config_data, file_rules]):
project_settings = project_settings or get_project_settings(
project_name
)
config_data = get_imageio_config(
project_name, host_name, project_settings)
file_rules = get_imageio_file_rules(
project_name, host_name, project_settings)
# match file rule from path
colorspace_name = None
for _frule_name, file_rule in file_rules.items():
pattern = file_rule["pattern"]
extension = file_rule["ext"]
ext_match = re.match(
r".*(?=.{})".format(extension), path
)
file_match = re.search(
pattern, path
)
if ext_match and file_match:
colorspace_name = file_rule["colorspace"]
if not colorspace_name:
log.info("No imageio file rule matched input path: '{}'".format(
path
))
return None
# validate matching colorspace with config
if validate and config_data:
validate_imageio_colorspace_in_config(
config_data["path"], colorspace_name)
return colorspace_name
def parse_colorspace_from_filepath(
path, host_name, project_name,
config_data=None,
project_settings=None
):
"""Parse colorspace name from filepath
An input path can have colorspace name used as part of name
or as folder name.
Args:
path (str): path string
host_name (str): host name
project_name (str): project name
config_data (dict, optional): config path and template in dict.
Defaults to None.
project_settings (dict, optional): project settings. Defaults to None.
Returns:
str: name of colorspace
"""
if not config_data:
project_settings = project_settings or get_project_settings(
project_name
)
config_data = get_imageio_config(
project_name, host_name, project_settings)
config_path = config_data["path"]
# match file rule from path
colorspace_name = None
colorspaces = get_ocio_config_colorspaces(config_path)
for colorspace_key in colorspaces:
# check underscored variant of colorspace name
# since we are reformating it in integrate.py
if colorspace_key.replace(" ", "_") in path:
colorspace_name = colorspace_key
break
if colorspace_key in path:
colorspace_name = colorspace_key
break
if not colorspace_name:
log.info("No matching colorspace in config '{}' for path: '{}'".format(
config_path, path
))
return None
return colorspace_name
def validate_imageio_colorspace_in_config(config_path, colorspace_name):
"""Validator making sure colorspace name is used in config.ocio
Args:
config_path (str): path leading to config.ocio file
colorspace_name (str): tested colorspace name
Raises:
KeyError: missing colorspace name
Returns:
bool: True if exists
"""
colorspaces = get_ocio_config_colorspaces(config_path)
if colorspace_name not in colorspaces:
raise KeyError(
"Missing colorspace '{}' in config file '{}'".format(
colorspace_name, config_path)
)
return True
def get_ocio_config_colorspaces(config_path):
"""Get all colorspace data
Wrapper function for aggregating all names and its families.
Families can be used for building menu and submenus in gui.
Args:
config_path (str): path leading to config.ocio file
Returns:
dict: colorspace and family in couple
"""
if sys.version_info[0] == 2:
return get_colorspace_data_subprocess(config_path)
from ..scripts.ocio_wrapper import get_colorspace_data
return get_colorspace_data(config_path)
def get_colorspace_data_subprocess(config_path):
"""Get colorspace data via subprocess
Wrapper for Python 2 hosts.
Args:
config_path (str): path leading to config.ocio file
Returns:
dict: colorspace and family in couple
"""
with _make_temp_json_file() as tmp_json_path:
# Prepare subprocess arguments
args = [
"run", get_ocio_config_script_path(),
"config", "get_colorspace",
"--in_path", config_path,
"--out_path", tmp_json_path
]
log.info("Executing: {}".format(" ".join(args)))
process_kwargs = {
"logger": log,
"env": {}
}
run_openpype_process(*args, **process_kwargs)
# return all colorspaces
return_json_data = open(tmp_json_path).read()
return json.loads(return_json_data)
def get_ocio_config_views(config_path):
"""Get all viewer data
Wrapper function for aggregating all display and related viewers.
Key can be used for building gui menu with submenus.
Args:
config_path (str): path leading to config.ocio file
Returns:
dict: `display/viewer` and viewer data
"""
if sys.version_info[0] == 2:
return get_views_data_subprocess(config_path)
from ..scripts.ocio_wrapper import get_views_data
return get_views_data(config_path)
def get_views_data_subprocess(config_path):
"""Get viewers data via subprocess
Wrapper for Python 2 hosts.
Args:
config_path (str): path leading to config.ocio file
Returns:
dict: `display/viewer` and viewer data
"""
with _make_temp_json_file() as tmp_json_path:
# Prepare subprocess arguments
args = [
"run", get_ocio_config_script_path(),
"config", "get_views",
"--in_path", config_path,
"--out_path", tmp_json_path
]
log.info("Executing: {}".format(" ".join(args)))
process_kwargs = {
"logger": log,
"env": {}
}
run_openpype_process(*args, **process_kwargs)
# return all colorspaces
return_json_data = open(tmp_json_path).read()
return json.loads(return_json_data)
def get_imageio_config(
project_name, host_name,
project_settings=None,
anatomy_data=None,
anatomy=None
):
"""Returns config data from settings
Config path is formatted in `path` key
and original settings input is saved into `template` key.
Args:
project_name (str): project name
host_name (str): host name
project_settings (dict, optional): project settings.
Defaults to None.
anatomy_data (dict, optional): anatomy formatting data.
Defaults to None.
anatomy (lib.Anatomy, optional): Anatomy object.
Defaults to None.
Returns:
dict or bool: config path data or None
"""
project_settings = project_settings or get_project_settings(project_name)
anatomy = anatomy or Anatomy(project_name)
current_platform = platform.system().lower()
if not anatomy_data:
from openpype.pipeline.context_tools import (
get_template_data_from_session)
anatomy_data = get_template_data_from_session()
# add project roots to anatomy data
anatomy_data["root"] = anatomy.roots
# get colorspace settings
imageio_global, imageio_host = _get_imageio_settings(
project_settings, host_name)
# get config path from either global or host_name
config_global = imageio_global["ocio_config"]
config_host = imageio_host["ocio_config"]
# set config path
config_path = None
if config_global["enabled"]:
config_path = config_global["filepath"][current_platform]
if config_host["enabled"]:
config_path = config_host["filepath"][current_platform]
if not config_path:
return
formatting_data = deepcopy(anatomy_data)
# format the path for potential env vars
formatting_data.update(dict(**os.environ))
# format path for anatomy keys
formatted_path = StringTemplate(config_path).format(
formatting_data)
abs_path = os.path.abspath(formatted_path)
return {
"path": os.path.normpath(abs_path),
"template": config_path
}
def get_imageio_file_rules(project_name, host_name, project_settings=None):
"""Get ImageIO File rules from project settings
Args:
project_name (str): project name
host_name (str): host name
project_settings (dict, optional): project settings.
Defaults to None.
Returns:
dict: file rules data
"""
project_settings = project_settings or get_project_settings(project_name)
imageio_global, imageio_host = _get_imageio_settings(
project_settings, host_name)
# get file rules from global and host_name
frules_global = imageio_global["file_rules"]
frules_host = imageio_host["file_rules"]
# compile file rules dictionary
file_rules = {}
if frules_global["enabled"]:
file_rules.update(frules_global["rules"])
if frules_host["enabled"]:
file_rules.update(frules_host["rules"])
return file_rules
def _get_imageio_settings(project_settings, host_name):
"""Get ImageIO settings for global and host
Args:
project_settings (dict): project settings.
Defaults to None.
host_name (str): host name
Returns:
tuple[dict, dict]: image io settings for global and host
"""
# get image io from global and host_name
imageio_global = project_settings["global"]["imageio"]
imageio_host = project_settings[host_name]["imageio"]
return imageio_global, imageio_host

View file

@ -19,6 +19,7 @@ from .publish_plugins import (
RepairContextAction,
Extractor,
ExtractorColormanaged,
)
from .lib import (
@ -63,6 +64,7 @@ __all__ = (
"RepairContextAction",
"Extractor",
"ExtractorColormanaged",
"get_publish_template_name",

View file

@ -1,6 +1,6 @@
import inspect
from abc import ABCMeta
from pprint import pformat
import pyblish.api
from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin
@ -13,6 +13,12 @@ from .lib import (
get_instance_staging_dir,
)
from openpype.pipeline.colorspace import (
get_imageio_colorspace_from_filepath,
get_imageio_config,
get_imageio_file_rules
)
class AbstractMetaInstancePlugin(ABCMeta, MetaPlugin):
pass
@ -250,12 +256,12 @@ class RepairContextAction(pyblish.api.Action):
if not hasattr(plugin, "repair"):
raise RuntimeError("Plug-in does not have repair method.")
# Get the errored instances
# Get the failed instances
self.log.info("Finding failed instances..")
errored_plugins = get_errored_plugins_from_context(context)
failed_plugins = get_errored_plugins_from_context(context)
# Apply pyblish.logic to get the instances for the plug-in
if plugin in errored_plugins:
if plugin in failed_plugins:
self.log.info("Attempting fix ...")
plugin.repair(context)
@ -280,3 +286,93 @@ class Extractor(pyblish.api.InstancePlugin):
"""
return get_instance_staging_dir(instance)
class ExtractorColormanaged(Extractor):
"""Extractor base for color managed image data.
Class implements a "set_representation_colorspace" function, which is used
for injecting colorspace data to representation data for farther
integration into db document.
"""
allowed_ext = [
"cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg",
"mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut",
"1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf",
"sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img"
]
@staticmethod
def get_colorspace_settings(context):
project_name = context.data["projectName"]
host_name = context.data["hostName"]
anatomy_data = context.data["anatomyData"]
project_settings_ = context.data["project_settings"]
config_data = get_imageio_config(
project_name, host_name,
project_settings=project_settings_,
anatomy_data=anatomy_data
)
file_rules = get_imageio_file_rules(
project_name, host_name,
project_settings=project_settings_
)
return config_data, file_rules
def set_representation_colorspace(
self, representation, context,
config_data, file_rules,
colorspace=None
):
if not config_data:
# warn in case no colorspace path was defined
self.log.warning("No colorspace management was defined")
return
self.log.info("Config data is : `{}`".format(
config_data))
ext = representation["ext"]
# check extension
self.log.debug("__ ext: `{}`".format(ext))
if ext.lower() not in self.allowed_ext:
return
project_name = context.data["projectName"]
host_name = context.data["hostName"]
project_settings = context.data["project_settings"]
# get one filename
filename = representation["files"]
if isinstance(filename, list):
filename = filename.pop()
self.log.debug("__ filename: `{}`".format(
filename))
# get matching colorspace from rules
colorspace = colorspace or get_imageio_colorspace_from_filepath(
filename, host_name, project_name,
config_data=config_data,
file_rules=file_rules,
project_settings=project_settings
)
self.log.debug("__ colorspace: `{}`".format(
colorspace))
# infuse data to representation
if colorspace:
colorspace_data = {
"colorspace": colorspace,
"configData": config_data
}
# update data key
representation["colorspaceData"] = colorspace_data
self.log.debug("__ colorspace_data: `{}`".format(
pformat(colorspace_data)))

View file

@ -0,0 +1,49 @@
import pyblish.api
from openpype.pipeline import publish
class ExtractColorspaceData(publish.ExtractorColormanaged):
""" Inject Colorspace data to available representations.
Input data:
- context.data[colorspace_config_path]:
for anatomy formatting of possible template tokens in config path
- context.data[colorspace_config_path]:
for resolving project and host related config.ocio
- context.data[colorspace_file_rules]:
for resolving matched file rule from representation file name
and adding it to representation
Output data:
representation[colorspaceData] = {
"colorspace": "linear",
"configData": {
"path": "/abs/path/to/config.ocio",
"template": "{project[root]}/path/to/config.ocio"
}
}
"""
label = "Extract Colorspace data"
order = pyblish.api.ExtractorOrder + 0.49
def process(self, instance):
representations = instance.data.get("representations")
if not representations:
self.log.info("No representations at instance : `{}`".format(
instance))
return
# get colorspace settings
context = instance.context
config_data, file_rules = self.get_colorspace_settings(context)
# loop representations
for representation in representations:
# skip if colorspaceData is already at representation
if representation.get("colorspaceData"):
continue
self.set_representation_colorspace(
representation, context,
config_data, file_rules
)

View file

@ -529,6 +529,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
template_data["representation"] = repre["name"]
template_data["ext"] = repre["ext"]
# add template data for colorspaceData
if repre.get("colorspaceData"):
colorspace = repre["colorspaceData"]["colorspace"]
# replace spaces with underscores
# pipeline.colorspace.parse_colorspace_from_filepath
# is checking it with underscores too
colorspace = colorspace.replace(" ", "_")
template_data["colorspace"] = colorspace
# optionals
# retrieve additional anatomy data from representation if exists
for key, anatomy_key in {
@ -726,6 +735,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
# and the actual representation entity for the database
data = repre.get("data", {})
data.update({"path": published_path, "template": template})
# add colorspace data if any exists on representation
if repre.get("colorspaceData"):
data["colorspaceData"] = repre["colorspaceData"]
repre_doc = new_representation_doc(
repre["name"], version["_id"], repre_context, data, repre_id
)

View file

@ -0,0 +1,168 @@
"""OpenColorIO Wrapper.
Only to be interpreted by Python 3. It is run in subprocess in case
Python 2 hosts needs to use it. Or it is used as module for Python 3
processing.
Providing functionality:
- get_colorspace - console command - python 2
- returning all available color spaces
found in input config path.
- get_colorspace_data - python 3 - module function
- returning all available colorspaces
found in input config path.
- get_views - console command - python 2
- returning all available viewers
found in input config path.
- get_views_data - python 3 - module function
- returning all available viewers
found in input config path.
"""
import click
import json
from pathlib2 import Path
import PyOpenColorIO as ocio
@click.group()
def main():
pass
@main.group()
def config():
"""Config related commands group
Example of use:
> pyton.exe ./ocio_wrapper.py config <command> *args
"""
pass
@config.command(
name="get_colorspace",
help=(
"return all colorspaces from config file "
"--path input arg is required"
)
)
@click.option("--in_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_colorspace(in_path, out_path):
"""Aggregate all colorspace to file.
Python 2 wrapped console command
Args:
in_path (str): config file path string
out_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py config get_colorspace
--in_path=<path> --out_path=<path>
"""
json_path = Path(out_path)
out_data = get_colorspace_data(in_path)
with open(json_path, "w") as f:
json.dump(out_data, f)
print(f"Colorspace data are saved to '{json_path}'")
def get_colorspace_data(config_path):
"""Return all found colorspace data.
Args:
config_path (str): path string leading to config.ocio
Raises:
IOError: Input config does not exist.
Returns:
dict: aggregated available colorspaces
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError(
f"Input path `{config_path}` should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
return {
c.getName(): c.getFamily()
for c in config.getColorSpaces()
}
@config.command(
name="get_views",
help=(
"return all viewers from config file "
"--path input arg is required"
)
)
@click.option("--in_path", required=True,
help="path where to read ocio config file",
type=click.Path(exists=True))
@click.option("--out_path", required=True,
help="path where to write output json file",
type=click.Path())
def get_views(in_path, out_path):
"""Aggregate all viewers to file.
Python 2 wrapped console command
Args:
in_path (str): config file path string
out_path (str): temp json file path string
Example of use:
> pyton.exe ./ocio_wrapper.py config get_views \
--in_path=<path> --out_path=<path>
"""
json_path = Path(out_path)
out_data = get_views_data(in_path)
with open(json_path, "w") as f:
json.dump(out_data, f)
print(f"Viewer data are saved to '{json_path}'")
def get_views_data(config_path):
"""Return all found viewer data.
Args:
config_path (str): path string leading to config.ocio
Raises:
IOError: Input config does not exist.
Returns:
dict: aggregated available viewers
"""
config_path = Path(config_path)
if not config_path.is_file():
raise IOError("Input path should be `config.ocio` file")
config = ocio.Config().CreateFromFile(str(config_path))
return {
f"{d}/{v}": {"display": d, "view": v}
for d in config.getDisplays()
for v in config.getViews(d)
}
if __name__ == '__main__':
main()

1720
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.14.2-nightly.2" # OpenPype
version = "3.14.8" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"
@ -70,6 +70,7 @@ requests = "^2.25.1"
pysftp = "^0.2.9"
dropbox = "^11.20.0"
aiohttp-middlewares = "^2.0.0"
opencolorio = "^2.2.0"
[tool.poetry.dev-dependencies]
flake8 = "^3.7"

View file

@ -14,6 +14,7 @@ import re
from tests.lib.db_handler import DBHandler
from common.openpype_common.distribution.file_handler import RemoteFileHandler
from openpype.modules import ModulesManager
from openpype.settings import get_project_settings
class BaseTest:
@ -28,6 +29,7 @@ class ModuleUnitTest(BaseTest):
Implemented fixtures:
monkeypatch_session - fixture for env vars with session scope
project_settings - fixture for project settings with session scope
download_test_data - tmp folder with extracted data from GDrive
env_var - sets env vars from input file
db_setup - prepares avalon AND openpype DBs for testing from
@ -59,6 +61,12 @@ class ModuleUnitTest(BaseTest):
yield m
m.undo()
@pytest.fixture(scope='module')
def project_settings(self):
yield get_project_settings(
self.PROJECT
)
@pytest.fixture(scope="module")
def download_test_data(self, test_data_folder, persist, request):
test_data_folder = test_data_folder or self.TEST_DATA_FOLDER
@ -67,6 +75,7 @@ class ModuleUnitTest(BaseTest):
yield test_data_folder
else:
tmpdir = tempfile.mkdtemp()
print("Temporary folder created:: {}".format(tmpdir))
for test_file in self.TEST_FILES:
file_id, file_name, md5 = test_file
@ -78,7 +87,6 @@ class ModuleUnitTest(BaseTest):
if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: # noqa: E501
RemoteFileHandler.unzip(os.path.join(tmpdir, file_name))
print("Temporary folder created:: {}".format(tmpdir))
yield tmpdir
persist = (persist or self.PERSIST or
@ -87,6 +95,15 @@ class ModuleUnitTest(BaseTest):
print("Removing {}".format(tmpdir))
shutil.rmtree(tmpdir)
@pytest.fixture(scope="module")
def output_folder_url(self, download_test_data):
"""Returns location of published data, cleans it first if exists."""
path = os.path.join(download_test_data, "output")
if os.path.exists(path):
print("Purging {}".format(path))
shutil.rmtree(path)
yield path
@pytest.fixture(scope="module")
def env_var(self, monkeypatch_session, download_test_data):
"""Sets temporary env vars from json file."""
@ -152,14 +169,27 @@ class ModuleUnitTest(BaseTest):
db_handler.teardown(self.TEST_OPENPYPE_NAME)
@pytest.fixture(scope="module")
def dbcon(self, db_setup):
def dbcon(self, db_setup, output_folder_url):
"""Provide test database connection.
Database prepared from dumps with 'db_setup' fixture.
"""
from openpype.pipeline import AvalonMongoDB
dbcon = AvalonMongoDB()
dbcon.Session["AVALON_PROJECT"] = self.TEST_PROJECT_NAME
dbcon.Session["AVALON_PROJECT"] = self.PROJECT
dbcon.Session["AVALON_ASSET"] = self.ASSET
dbcon.Session["AVALON_TASK"] = self.TASK
# set project root to temp folder
platform_str = platform.system().lower()
root_key = "config.roots.work.{}".format(platform_str)
dbcon.update_one(
{"type": "project"},
{"$set":
{
root_key: output_folder_url
}}
)
yield dbcon
@pytest.fixture(scope="module")
@ -228,15 +258,6 @@ class PublishTest(ModuleUnitTest):
yield "{}/{}".format(self.APP_GROUP, app_variant)
@pytest.fixture(scope="module")
def output_folder_url(self, download_test_data):
"""Returns location of published data, cleans it first if exists."""
path = os.path.join(download_test_data, "output")
if os.path.exists(path):
print("Purging {}".format(path))
shutil.rmtree(path)
yield path
@pytest.fixture(scope="module")
def app_args(self, download_test_data):
"""Returns additional application arguments from a test file.
@ -267,17 +288,6 @@ class PublishTest(ModuleUnitTest):
def launched_app(self, dbcon, download_test_data, last_workfile_path,
startup_scripts, app_args, app_name, output_folder_url):
"""Launch host app"""
# set publishing folders
platform_str = platform.system().lower()
root_key = "config.roots.work.{}".format(platform_str)
dbcon.update_one(
{"type": "project"},
{"$set":
{
root_key: output_folder_url
}}
)
# set schema - for integrate_new
from openpype import PACKAGE_DIR
# Path to OpenPype's schema

View file

@ -0,0 +1,13 @@
import pytest
from tests.lib.testing_classes import ModuleUnitTest
from openpype.pipeline import legacy_io
class TestPipeline(ModuleUnitTest):
""" Testing Pipeline base class
"""
@pytest.fixture(scope="module")
def legacy_io(self, dbcon):
legacy_io.Session = dbcon.Session
yield legacy_io.Session

View file

@ -0,0 +1,203 @@
"""Test Publish_plugins pipeline publish modul, tests API methods
File:
creates temporary directory and downloads .zip file from GDrive
unzips .zip file
uses content of .zip file (MongoDB's dumps) to import to new databases
with use of 'monkeypatch_session' modifies required env vars
temporarily
runs battery of tests checking that site operation for Sync Server
module are working
removes temporary folder
removes temporary databases (?)
"""
import os
import pytest
import shutil
from tests.unit.openpype.pipeline.lib import TestPipeline
from openpype.pipeline.publish import publish_plugins
from openpype.pipeline import colorspace
class TestPipelinePublishPlugins(TestPipeline):
""" Testing Pipeline pubish_plugins.py
Example:
cd to OpenPype repo root dir
poetry run python ./start.py runtests \
../tests/unit/openpype/pipeline/publish
"""
# files are the same as those used in `test_pipeline_colorspace`
TEST_FILES = [
(
"1kJ1ZYcf7V7jS8IW2routSYQoGUfUWj4F",
"test_pipeline_colorspace.zip",
""
)
]
@pytest.fixture(scope="module")
def context(self, legacy_io, project_settings):
class CTX:
data = {
"projectName": legacy_io["AVALON_PROJECT"],
"asset": legacy_io["AVALON_ASSET"],
"hostName": "nuke",
"anatomyData": {},
"project_settings": project_settings
}
yield CTX
@pytest.fixture(scope="module")
def config_path_project(
self,
download_test_data,
output_folder_url
):
src_path = os.path.join(
download_test_data,
"input",
"data",
"configs",
"aces_1.3",
"ayon_aces_config_project.ocio"
)
dest_dir = os.path.join(
output_folder_url,
self.PROJECT,
"ocio"
)
dest_path = os.path.join(
dest_dir,
"config.ocio"
)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
shutil.copyfile(src_path, dest_path)
yield dest_path
@pytest.fixture(scope="module")
def config_path_asset(
self,
download_test_data,
output_folder_url
):
src_path = os.path.join(
download_test_data,
"input",
"data",
"configs",
"aces_1.3",
"ayon_aces_config_asset.ocio"
)
dest_dir = os.path.join(
output_folder_url,
self.PROJECT,
self.ASSET,
"ocio"
)
dest_path = os.path.join(
dest_dir,
"config.ocio"
)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
shutil.copyfile(src_path, dest_path)
yield dest_path
def test_get_colorspace_settings(self, context):
expected_config_template = (
"{root[work]}/{project[name]}/{asset}/ocio/config.ocio")
expected_file_rules = {
"comp_review": {
"pattern": "renderCompMain.baking_h264",
"colorspace": "Camera Rec.709",
"ext": "mp4"
}
}
# load plugin function for testing
plugin = publish_plugins.ExtractorColormanaged()
config_data, file_rules = plugin.get_colorspace_settings(context)
assert config_data["template"] == expected_config_template, (
"Returned config tempate is not "
f"matching {expected_config_template}"
)
assert file_rules == expected_file_rules, (
"Returned file rules are not "
f"matching {expected_file_rules}"
)
def test_set_representation_colorspace(
self, context, project_settings,
config_path_project, config_path_asset
):
expected_colorspace_hiero = "sRGB - Texture"
expected_colorspace_nuke = "Camera Rec.709"
config_data_nuke = colorspace.get_imageio_config(
"test_project", "nuke", project_settings)
file_rules_nuke = colorspace.get_imageio_file_rules(
"test_project", "nuke", project_settings)
config_data_hiero = colorspace.get_imageio_config(
"test_project", "hiero", project_settings)
file_rules_hiero = colorspace.get_imageio_file_rules(
"test_project", "hiero", project_settings)
representation_nuke = {
"ext": "mp4",
"files": "this_file_renderCompMain.baking_h264.mp4"
}
representation_hiero = {
"ext": "mp4",
"files": "this_file_renderCompMain_h264burninburnin.mp4"
}
# load plugin function for testing
plugin = publish_plugins.ExtractorColormanaged()
plugin.set_representation_colorspace(
representation_nuke, context,
config_data_nuke, file_rules_nuke
)
# load plugin function for testing
plugin = publish_plugins.ExtractorColormanaged()
plugin.set_representation_colorspace(
representation_hiero, context,
config_data_hiero, file_rules_hiero
)
colorspace_data_nuke = representation_nuke.get("colorspaceData")
colorspace_data_hiero = representation_hiero.get("colorspaceData")
assert colorspace_data_nuke, (
"Colorspace data were not created in prepresentation"
f"matching {representation_nuke}"
)
assert colorspace_data_hiero, (
"Colorspace data were not created in prepresentation"
f"matching {representation_hiero}"
)
ret_colorspace_nuke = colorspace_data_nuke["colorspace"]
assert ret_colorspace_nuke == expected_colorspace_nuke, (
"Returned colorspace is not "
f"matching {expected_colorspace_nuke}"
)
ret_colorspace_hiero = colorspace_data_hiero["colorspace"]
assert ret_colorspace_hiero == expected_colorspace_hiero, (
"Returned colorspace is not "
f"matching {expected_colorspace_hiero}"
)
test_case = TestPipelinePublishPlugins()

View file

@ -0,0 +1,185 @@
"""Test Colorspace pipeline modul, tests API methods
File:
creates temporary directory and downloads .zip file from GDrive
unzips .zip file
uses content of .zip file (MongoDB's dumps) to import to new databases
with use of 'monkeypatch_session' modifies required env vars
temporarily
runs battery of tests checking that site operation for Sync Server
module are working
removes temporary folder
removes temporary databases (?)
"""
import pytest
import shutil
import os
from tests.unit.openpype.pipeline.lib import TestPipeline
from openpype.pipeline import colorspace
class TestPipelineColorspace(TestPipeline):
""" Testing Colorspace
Example:
cd to OpenPype repo root dir
poetry run python ./start.py runtests ../tests/unit/openpype/pipeline
"""
TEST_FILES = [
(
"1kJ1ZYcf7V7jS8IW2routSYQoGUfUWj4F",
"test_pipeline_colorspace.zip",
""
)
]
@pytest.fixture(scope="module")
def config_path_project(
self,
download_test_data,
output_folder_url
):
src_path = os.path.join(
download_test_data,
"input",
"data",
"configs",
"aces_1.3",
"ayon_aces_config_project.ocio"
)
dest_dir = os.path.join(
output_folder_url,
self.PROJECT,
"ocio"
)
dest_path = os.path.join(
dest_dir,
"config.ocio"
)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
shutil.copyfile(src_path, dest_path)
yield dest_path
@pytest.fixture(scope="module")
def config_path_asset(
self,
download_test_data,
output_folder_url
):
src_path = os.path.join(
download_test_data,
"input",
"data",
"configs",
"aces_1.3",
"ayon_aces_config_asset.ocio"
)
dest_dir = os.path.join(
output_folder_url,
self.PROJECT,
self.ASSET,
"ocio"
)
dest_path = os.path.join(
dest_dir,
"config.ocio"
)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
shutil.copyfile(src_path, dest_path)
yield dest_path
def test_config_file_project(
self,
legacy_io,
config_path_project,
project_settings
):
expected_template = "{root[work]}/{project[name]}/ocio/config.ocio"
# get config_data from hiero
# where project level config is defined
config_data = colorspace.get_imageio_config(
"test_project", "hiero", project_settings)
assert os.path.exists(config_data["path"]), (
f"Config file \'{config_data['path']}\' doesn't exist"
)
assert config_data["template"] == expected_template, (
f"Config template \'{config_data['template']}\' doesn't match "
f"expected tempalte \'{expected_template}\'"
)
def test_parse_colorspace_from_filepath(
self,
legacy_io,
config_path_asset,
project_settings
):
path_1 = "renderCompMain_ACES2065-1.####.exr"
expected_1 = "ACES2065-1"
ret_1 = colorspace.parse_colorspace_from_filepath(
path_1, "nuke", "test_project", project_settings=project_settings
)
assert ret_1 == expected_1, f"Not matching colorspace {expected_1}"
path_2 = "renderCompMain_BMDFilm_WideGamut_Gen5.mov"
expected_2 = "BMDFilm WideGamut Gen5"
ret_2 = colorspace.parse_colorspace_from_filepath(
path_2, "nuke", "test_project", project_settings=project_settings
)
assert ret_2 == expected_2, f"Not matching colorspace {expected_2}"
def test_get_ocio_config_views_asset(self, config_path_asset):
expected_num_keys = 12
ret = colorspace.get_ocio_config_views(config_path_asset)
assert len(ret) == expected_num_keys, (
f"Not matching num viewer keys {expected_num_keys}")
def test_get_ocio_config_views_project(self, config_path_project):
expected_num_keys = 3
ret = colorspace.get_ocio_config_views(config_path_project)
assert len(ret) == expected_num_keys, (
f"Not matching num viewer keys {expected_num_keys}")
def test_file_rules(self, project_settings):
expected_nuke = {
"comp_review": {
"pattern": "renderCompMain.baking_h264",
"colorspace": "Camera Rec.709",
"ext": "mp4"
}
}
expected_hiero = {
"comp_review": {
"pattern": "renderCompMain_h264burninburnin",
"colorspace": "sRGB - Texture",
"ext": "mp4"
}
}
nuke_file_rules = colorspace.get_imageio_file_rules(
"test_project", "nuke", project_settings=project_settings)
assert expected_nuke == nuke_file_rules, (
f"Not matching file rules {expected_nuke}")
hiero_file_rules = colorspace.get_imageio_file_rules(
"test_project", "hiero", project_settings=project_settings)
assert expected_hiero == hiero_file_rules, (
f"Not matching file rules {expected_hiero}")
test_case = TestPipelineColorspace()