[Automated] Merged develop into main

This commit is contained in:
pypebot 2022-01-08 04:37:22 +01:00 committed by GitHub
commit 90b93dd92c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1784 additions and 347 deletions

View file

@ -6,9 +6,18 @@ import sys
os.chdir(os.path.dirname(__file__)) # for override sys.path in Deadline
from .bootstrap_repos import BootstrapRepos
from .bootstrap_repos import (
BootstrapRepos,
OpenPypeVersion
)
from .version import __version__ as version
# Store OpenPypeVersion to 'sys.modules'
# - this makes it available in OpenPype processes without modifying
# 'sys.path' or 'PYTHONPATH'
if "OpenPypeVersion" not in sys.modules:
sys.modules["OpenPypeVersion"] = OpenPypeVersion
def open_dialog():
"""Show Igniter dialog."""
@ -22,7 +31,9 @@ def open_dialog():
if scale_attr is not None:
QtWidgets.QApplication.setAttribute(scale_attr)
app = QtWidgets.QApplication(sys.argv)
app = QtWidgets.QApplication.instance()
if not app:
app = QtWidgets.QApplication(sys.argv)
d = InstallDialog()
d.open()
@ -43,7 +54,9 @@ def open_update_window(openpype_version):
if scale_attr is not None:
QtWidgets.QApplication.setAttribute(scale_attr)
app = QtWidgets.QApplication(sys.argv)
app = QtWidgets.QApplication.instance()
if not app:
app = QtWidgets.QApplication(sys.argv)
d = UpdateWindow(version=openpype_version)
d.open()
@ -53,9 +66,32 @@ def open_update_window(openpype_version):
return version_path
def show_message_dialog(title, message):
"""Show dialog with a message and title to user."""
if os.getenv("OPENPYPE_HEADLESS_MODE"):
print("!!! Can't open dialog in headless mode. Exiting.")
sys.exit(1)
from Qt import QtWidgets, QtCore
from .message_dialog import MessageDialog
scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None)
if scale_attr is not None:
QtWidgets.QApplication.setAttribute(scale_attr)
app = QtWidgets.QApplication.instance()
if not app:
app = QtWidgets.QApplication(sys.argv)
dialog = MessageDialog(title, message)
dialog.open()
app.exec_()
__all__ = [
"BootstrapRepos",
"open_dialog",
"open_update_window",
"show_message_dialog",
"version"
]

View file

@ -22,7 +22,10 @@ from .user_settings import (
OpenPypeSecureRegistry,
OpenPypeSettingsRegistry
)
from .tools import get_openpype_path_from_db
from .tools import (
get_openpype_path_from_db,
get_expected_studio_version_str
)
LOG_INFO = 0
@ -60,6 +63,7 @@ class OpenPypeVersion(semver.VersionInfo):
staging = False
path = None
_VERSION_REGEX = re.compile(r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$") # noqa: E501
_installed_version = None
def __init__(self, *args, **kwargs):
"""Create OpenPype version.
@ -232,6 +236,390 @@ class OpenPypeVersion(semver.VersionInfo):
else:
return hash(str(self))
@staticmethod
def is_version_in_dir(
dir_item: Path, version: OpenPypeVersion) -> Tuple[bool, str]:
"""Test if path item is OpenPype version matching detected version.
If item is directory that might (based on it's name)
contain OpenPype version, check if it really does contain
OpenPype and that their versions matches.
Args:
dir_item (Path): Directory to test.
version (OpenPypeVersion): OpenPype version detected
from name.
Returns:
Tuple: State and reason, True if it is valid OpenPype version,
False otherwise.
"""
try:
# add one 'openpype' level as inside dir there should
# be many other repositories.
version_str = OpenPypeVersion.get_version_string_from_directory(
dir_item) # noqa: E501
version_check = OpenPypeVersion(version=version_str)
except ValueError:
return False, f"cannot determine version from {dir_item}"
version_main = version_check.get_main_version()
detected_main = version.get_main_version()
if version_main != detected_main:
return False, (f"dir version ({version}) and "
f"its content version ({version_check}) "
"doesn't match. Skipping.")
return True, "Versions match"
@staticmethod
def is_version_in_zip(
zip_item: Path, version: OpenPypeVersion) -> Tuple[bool, str]:
"""Test if zip path is OpenPype version matching detected version.
Open zip file, look inside and parse version from OpenPype
inside it. If there is none, or it is different from
version specified in file name, skip it.
Args:
zip_item (Path): Zip file to test.
version (OpenPypeVersion): Pype version detected
from name.
Returns:
Tuple: State and reason, True if it is valid OpenPype version,
False otherwise.
"""
# skip non-zip files
if zip_item.suffix.lower() != ".zip":
return False, "Not a zip"
try:
with ZipFile(zip_item, "r") as zip_file:
with zip_file.open(
"openpype/version.py") as version_file:
zip_version = {}
exec(version_file.read(), zip_version)
try:
version_check = OpenPypeVersion(
version=zip_version["__version__"])
except ValueError as e:
return False, str(e)
version_main = version_check.get_main_version() #
# noqa: E501
detected_main = version.get_main_version()
# noqa: E501
if version_main != detected_main:
return False, (f"zip version ({version}) "
f"and its content version "
f"({version_check}) "
"doesn't match. Skipping.")
except BadZipFile:
return False, f"{zip_item} is not a zip file"
except KeyError:
return False, "Zip does not contain OpenPype"
return True, "Versions match"
@staticmethod
def get_version_string_from_directory(repo_dir: Path) -> Union[str, None]:
"""Get version of OpenPype in given directory.
Note: in frozen OpenPype installed in user data dir, this must point
one level deeper as it is:
`openpype-version-v3.0.0/openpype/version.py`
Args:
repo_dir (Path): Path to OpenPype repo.
Returns:
str: version string.
None: if OpenPype is not found.
"""
# try to find version
version_file = Path(repo_dir) / "openpype" / "version.py"
if not version_file.exists():
return None
version = {}
with version_file.open("r") as fp:
exec(fp.read(), version)
return version['__version__']
@classmethod
def get_openpype_path(cls):
"""Path to openpype zip directory.
Path can be set through environment variable 'OPENPYPE_PATH' which
is set during start of OpenPype if is not available.
"""
return os.getenv("OPENPYPE_PATH")
@classmethod
def openpype_path_is_set(cls):
"""Path to OpenPype zip directory is set."""
if cls.get_openpype_path():
return True
return False
@classmethod
def openpype_path_is_accessible(cls):
"""Path to OpenPype zip directory is accessible.
Exists for this machine.
"""
# First check if is set
if not cls.openpype_path_is_set():
return False
# Validate existence
if Path(cls.get_openpype_path()).exists():
return True
return False
@classmethod
def get_local_versions(
cls, production: bool = None, staging: bool = None
) -> List:
"""Get all versions available on this machine.
Arguments give ability to specify if filtering is needed. If both
arguments are set to None all found versions are returned.
Args:
production (bool): Return production versions.
staging (bool): Return staging versions.
"""
# Return all local versions if arguments are set to None
if production is None and staging is None:
production = True
staging = True
elif production is None and not staging:
production = True
elif staging is None and not production:
staging = True
# Just return empty output if both are disabled
if not production and not staging:
return []
dir_to_search = Path(user_data_dir("openpype", "pypeclub"))
versions = OpenPypeVersion.get_versions_from_directory(
dir_to_search
)
filtered_versions = []
for version in versions:
if version.is_staging():
if staging:
filtered_versions.append(version)
elif production:
filtered_versions.append(version)
return list(sorted(set(filtered_versions)))
@classmethod
def get_remote_versions(
cls, production: bool = None, staging: bool = None
) -> List:
"""Get all versions available in OpenPype Path.
Arguments give ability to specify if filtering is needed. If both
arguments are set to None all found versions are returned.
Args:
production (bool): Return production versions.
staging (bool): Return staging versions.
"""
# Return all local versions if arguments are set to None
if production is None and staging is None:
production = True
staging = True
elif production is None and not staging:
production = True
elif staging is None and not production:
staging = True
# Just return empty output if both are disabled
if not production and not staging:
return []
dir_to_search = None
if cls.openpype_path_is_accessible():
dir_to_search = Path(cls.get_openpype_path())
else:
registry = OpenPypeSettingsRegistry()
try:
registry_dir = Path(str(registry.get_item("openPypePath")))
if registry_dir.exists():
dir_to_search = registry_dir
except ValueError:
# nothing found in registry, we'll use data dir
pass
if not dir_to_search:
return []
versions = cls.get_versions_from_directory(dir_to_search)
filtered_versions = []
for version in versions:
if version.is_staging():
if staging:
filtered_versions.append(version)
elif production:
filtered_versions.append(version)
return list(sorted(set(filtered_versions)))
@staticmethod
def get_versions_from_directory(openpype_dir: Path) -> List:
"""Get all detected OpenPype versions in directory.
Args:
openpype_dir (Path): Directory to scan.
Returns:
list of OpenPypeVersion
Throws:
ValueError: if invalid path is specified.
"""
if not openpype_dir.exists() and not openpype_dir.is_dir():
raise ValueError("specified directory is invalid")
_openpype_versions = []
# iterate over directory in first level and find all that might
# contain OpenPype.
for item in openpype_dir.iterdir():
# if file, strip extension, in case of dir not.
name = item.name if item.is_dir() else item.stem
result = OpenPypeVersion.version_in_str(name)
if result:
detected_version: OpenPypeVersion
detected_version = result
if item.is_dir() and not OpenPypeVersion.is_version_in_dir(
item, detected_version
)[0]:
continue
if item.is_file() and not OpenPypeVersion.is_version_in_zip(
item, detected_version
)[0]:
continue
detected_version.path = item
_openpype_versions.append(detected_version)
return sorted(_openpype_versions)
@staticmethod
def get_installed_version_str() -> str:
"""Get version of local OpenPype."""
version = {}
path = Path(os.environ["OPENPYPE_ROOT"]) / "openpype" / "version.py"
with open(path, "r") as fp:
exec(fp.read(), version)
return version["__version__"]
@classmethod
def get_installed_version(cls):
"""Get version of OpenPype inside build."""
if cls._installed_version is None:
installed_version_str = cls.get_installed_version_str()
if installed_version_str:
cls._installed_version = OpenPypeVersion(
version=installed_version_str,
path=Path(os.environ["OPENPYPE_ROOT"])
)
return cls._installed_version
@staticmethod
def get_latest_version(
staging: bool = False,
local: bool = None,
remote: bool = None
) -> OpenPypeVersion:
"""Get latest available version.
The version does not contain information about path and source.
This is utility version to get latest version from all found. Build
version is not listed if staging is enabled.
Arguments 'local' and 'remote' define if local and remote repository
versions are used. All versions are used if both are not set (or set
to 'None'). If only one of them is set to 'True' the other is disabled.
It is possible to set both to 'True' (same as both set to None) and to
'False' in that case only build version can be used.
Args:
staging (bool, optional): List staging versions if True.
local (bool, optional): List local versions if True.
remote (bool, optional): List remote versions if True.
"""
if local is None and remote is None:
local = True
remote = True
elif local is None and not remote:
local = True
elif remote is None and not local:
remote = True
installed_version = OpenPypeVersion.get_installed_version()
local_versions = []
remote_versions = []
if local:
local_versions = OpenPypeVersion.get_local_versions(
staging=staging
)
if remote:
remote_versions = OpenPypeVersion.get_remote_versions(
staging=staging
)
all_versions = local_versions + remote_versions
if not staging:
all_versions.append(installed_version)
if not all_versions:
return None
all_versions.sort()
return all_versions[-1]
@classmethod
def get_expected_studio_version(cls, staging=False, global_settings=None):
"""Expected OpenPype version that should be used at the moment.
If version is not defined in settings the latest found version is
used.
Using precached global settings is needed for usage inside OpenPype.
Args:
staging (bool): Staging version or production version.
global_settings (dict): Optional precached global settings.
Returns:
OpenPypeVersion: Version that should be used.
"""
result = get_expected_studio_version_str(staging, global_settings)
if not result:
return None
return OpenPypeVersion(version=result)
class BootstrapRepos:
"""Class for bootstrapping local OpenPype installation.
@ -301,16 +689,6 @@ class BootstrapRepos:
return v.path
return None
@staticmethod
def get_local_live_version() -> str:
"""Get version of local OpenPype."""
version = {}
path = Path(os.environ["OPENPYPE_ROOT"]) / "openpype" / "version.py"
with open(path, "r") as fp:
exec(fp.read(), version)
return version["__version__"]
@staticmethod
def get_version(repo_dir: Path) -> Union[str, None]:
"""Get version of OpenPype in given directory.
@ -358,7 +736,7 @@ class BootstrapRepos:
# version and use it as a source. Otherwise repo_dir is user
# entered location.
if not repo_dir:
version = self.get_local_live_version()
version = OpenPypeVersion.get_installed_version_str()
repo_dir = self.live_repo_dir
else:
version = self.get_version(repo_dir)
@ -734,6 +1112,65 @@ class BootstrapRepos:
os.environ["PYTHONPATH"] = os.pathsep.join(paths)
@staticmethod
def find_openpype_version(version, staging):
if isinstance(version, str):
version = OpenPypeVersion(version=version)
installed_version = OpenPypeVersion.get_installed_version()
if installed_version == version:
return installed_version
local_versions = OpenPypeVersion.get_local_versions(
staging=staging, production=not staging
)
zip_version = None
for local_version in local_versions:
if local_version == version:
if local_version.path.suffix.lower() == ".zip":
zip_version = local_version
else:
return local_version
if zip_version is not None:
return zip_version
remote_versions = OpenPypeVersion.get_remote_versions(
staging=staging, production=not staging
)
for remote_version in remote_versions:
if remote_version == version:
return remote_version
return None
@staticmethod
def find_latest_openpype_version(staging):
installed_version = OpenPypeVersion.get_installed_version()
local_versions = OpenPypeVersion.get_local_versions(
staging=staging
)
remote_versions = OpenPypeVersion.get_remote_versions(
staging=staging
)
all_versions = local_versions + remote_versions
if not staging:
all_versions.append(installed_version)
if not all_versions:
return None
all_versions.sort()
latest_version = all_versions[-1]
if latest_version == installed_version:
return latest_version
if not latest_version.path.is_dir():
for version in local_versions:
if version == latest_version and version.path.is_dir():
latest_version = version
break
return latest_version
def find_openpype(
self,
openpype_path: Union[Path, str] = None,

View file

@ -12,7 +12,8 @@ from Qt.QtCore import QTimer # noqa
from .install_thread import InstallThread
from .tools import (
validate_mongo_connection,
get_openpype_path_from_db
get_openpype_path_from_db,
get_openpype_icon_path
)
from .nice_progress_bar import NiceProgressBar
@ -187,7 +188,6 @@ class InstallDialog(QtWidgets.QDialog):
current_dir = os.path.dirname(os.path.abspath(__file__))
roboto_font_path = os.path.join(current_dir, "RobotoMono-Regular.ttf")
poppins_font_path = os.path.join(current_dir, "Poppins")
icon_path = os.path.join(current_dir, "openpype_icon.png")
# Install roboto font
QtGui.QFontDatabase.addApplicationFont(roboto_font_path)
@ -196,6 +196,7 @@ class InstallDialog(QtWidgets.QDialog):
QtGui.QFontDatabase.addApplicationFont(filename)
# Load logo
icon_path = get_openpype_icon_path()
pixmap_openpype_logo = QtGui.QPixmap(icon_path)
# Set logo as icon of window
self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo))

44
igniter/message_dialog.py Normal file
View file

@ -0,0 +1,44 @@
from Qt import QtWidgets, QtGui
from .tools import (
load_stylesheet,
get_openpype_icon_path
)
class MessageDialog(QtWidgets.QDialog):
"""Simple message dialog with title, message and OK button."""
def __init__(self, title, message):
super(MessageDialog, self).__init__()
# Set logo as icon of window
icon_path = get_openpype_icon_path()
pixmap_openpype_logo = QtGui.QPixmap(icon_path)
self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo))
# Set title
self.setWindowTitle(title)
# Set message
label_widget = QtWidgets.QLabel(message, self)
ok_btn = QtWidgets.QPushButton("OK", self)
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.addStretch(1)
btns_layout.addWidget(ok_btn, 0)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(label_widget, 1)
layout.addLayout(btns_layout, 0)
ok_btn.clicked.connect(self._on_ok_clicked)
self._label_widget = label_widget
self._ok_btn = ok_btn
def _on_ok_clicked(self):
self.close()
def showEvent(self, event):
super(MessageDialog, self).showEvent(event)
self.setStyleSheet(load_stylesheet())

View file

@ -16,6 +16,11 @@ from pymongo.errors import (
)
class OpenPypeVersionNotFound(Exception):
"""OpenPype version was not found in remote and local repository."""
pass
def should_add_certificate_path_to_mongo_url(mongo_url):
"""Check if should add ca certificate to mongo url.
@ -182,6 +187,28 @@ def get_openpype_path_from_db(url: str) -> Union[str, None]:
return None
def get_expected_studio_version_str(
staging=False, global_settings=None
) -> str:
"""Version that should be currently used in studio.
Args:
staging (bool): Get current version for staging.
global_settings (dict): Optional precached global settings.
Returns:
str: OpenPype version which should be used. Empty string means latest.
"""
mongo_url = os.environ.get("OPENPYPE_MONGO")
if global_settings is None:
global_settings = get_openpype_global_settings(mongo_url)
if staging:
key = "staging_version"
else:
key = "production_version"
return global_settings.get(key) or ""
def load_stylesheet() -> str:
"""Load css style sheet.
@ -192,3 +219,11 @@ def load_stylesheet() -> str:
stylesheet_path = Path(__file__).parent.resolve() / "stylesheet.css"
return stylesheet_path.read_text()
def get_openpype_icon_path() -> str:
"""Path to OpenPype icon png file."""
return os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"openpype_icon.png"
)

View file

@ -72,7 +72,7 @@ class RepairContextAction(pyblish.api.Action):
is available on the plugin.
"""
label = "Repair Context"
label = "Repair"
on = "failed" # This action is only available on a failed plug-in
def process(self, context, plugin):

View file

@ -1,6 +1,7 @@
import openpype.api
from Qt import QtWidgets
from avalon import aftereffects
from avalon.api import CreatorError
import openpype.api
import logging
@ -27,14 +28,13 @@ class CreateRender(openpype.api.Creator):
folders=False,
footages=False)
if len(items) > 1:
self._show_msg("Please select only single composition at time.")
return False
raise CreatorError("Please select only single "
"composition at time.")
if not items:
self._show_msg("Nothing to create. Select composition " +
"if 'useSelection' or create at least " +
"one composition.")
return False
raise CreatorError("Nothing to create. Select composition " +
"if 'useSelection' or create at least " +
"one composition.")
existing_subsets = [instance['subset'].lower()
for instance in aftereffects.list_instances()]
@ -42,8 +42,7 @@ class CreateRender(openpype.api.Creator):
item = items.pop()
if self.name.lower() in existing_subsets:
txt = "Instance with name \"{}\" already exists.".format(self.name)
self._show_msg(txt)
return False
raise CreatorError(txt)
self.data["members"] = [item.id]
self.data["uuid"] = item.id # for SubsetManager
@ -54,9 +53,3 @@ class CreateRender(openpype.api.Creator):
stub.imprint(item, self.data)
stub.set_label_color(item.id, 14) # Cyan options 0 - 16
stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"])
def _show_msg(self, txt):
msg = QtWidgets.QMessageBox()
msg.setIcon(QtWidgets.QMessageBox.Warning)
msg.setText(txt)
msg.exec_()

View file

@ -22,21 +22,23 @@ class BackgroundLoader(api.Loader):
def load(self, context, name=None, namespace=None, data=None):
items = stub.get_items(comps=True)
existing_items = [layer.name for layer in items]
existing_items = [layer.name.replace(stub.LOADED_ICON, '')
for layer in items]
comp_name = get_unique_layer_name(
existing_items,
"{}_{}".format(context["asset"]["name"], name))
layers = get_background_layers(self.fname)
if not layers:
raise ValueError("No layers found in {}".format(self.fname))
comp = stub.import_background(None, stub.LOADED_ICON + comp_name,
layers)
if not comp:
self.log.warning(
"Import background failed.")
self.log.warning("Check host app for alert error.")
return
raise ValueError("Import background failed. "
"Please contact support")
self[:] = [comp]
namespace = namespace or comp_name

View file

@ -7,21 +7,6 @@ from avalon import maya
from openpype.hosts.maya.api import lib
def polyConstraint(objects, *args, **kwargs):
kwargs.pop('mode', None)
with lib.no_undo(flush=False):
with maya.maintained_selection():
with lib.reset_polySelectConstraint():
cmds.select(objects, r=1, noExpand=True)
# Acting as 'polyCleanupArgList' for n-sided polygon selection
cmds.polySelectConstraint(*args, mode=3, **kwargs)
result = cmds.ls(selection=True)
cmds.select(clear=True)
return result
class ValidateMeshNgons(pyblish.api.Validator):
"""Ensure that meshes don't have ngons
@ -41,8 +26,17 @@ class ValidateMeshNgons(pyblish.api.Validator):
@staticmethod
def get_invalid(instance):
meshes = cmds.ls(instance, type='mesh')
return polyConstraint(meshes, type=8, size=3)
meshes = cmds.ls(instance, type='mesh', long=True)
# Get all faces
faces = ['{0}.f[*]'.format(node) for node in meshes]
# Filter to n-sided polygon faces (ngons)
invalid = lib.polyConstraint(faces,
t=0x0008, # type=face
size=3) # size=nsided
return invalid
def process(self, instance):
"""Process all the nodes in the instance "objectSet"""

View file

@ -1568,8 +1568,11 @@ class Roots:
key_items = [self.env_prefix]
for _key in keys:
key_items.append(_key.upper())
key = "_".join(key_items)
return {key: roots.value}
# Make sure key and value does not contain unicode
# - can happen in Python 2 hosts
return {str(key): str(roots.value)}
output = {}
for _key, _value in roots.items():

View file

@ -0,0 +1,85 @@
"""Lib access to OpenPypeVersion from igniter.
Access to logic from igniter is available only for OpenPype processes.
Is meant to be able check OpenPype versions for studio. The logic is dependent
on igniter's inner logic of versions.
Keep in mind that all functions except 'get_installed_version' does not return
OpenPype version located in build but versions available in remote versions
repository or locally available.
"""
import sys
def get_OpenPypeVersion():
"""Access to OpenPypeVersion class stored in sys modules."""
return sys.modules.get("OpenPypeVersion")
def op_version_control_available():
"""Check if current process has access to OpenPypeVersion."""
if get_OpenPypeVersion() is None:
return False
return True
def get_installed_version():
"""Get OpenPype version inside build.
This version is not returned by any other functions here.
"""
if op_version_control_available():
return get_OpenPypeVersion().get_installed_version()
return None
def get_available_versions(*args, **kwargs):
"""Get list of available versions."""
if op_version_control_available():
return get_OpenPypeVersion().get_available_versions(
*args, **kwargs
)
return None
def openpype_path_is_set():
"""OpenPype repository path is set in settings."""
if op_version_control_available():
return get_OpenPypeVersion().openpype_path_is_set()
return None
def openpype_path_is_accessible():
"""OpenPype version repository path can be accessed."""
if op_version_control_available():
return get_OpenPypeVersion().openpype_path_is_accessible()
return None
def get_local_versions(*args, **kwargs):
"""OpenPype versions available on this workstation."""
if op_version_control_available():
return get_OpenPypeVersion().get_local_versions(*args, **kwargs)
return None
def get_remote_versions(*args, **kwargs):
"""OpenPype versions in repository path."""
if op_version_control_available():
return get_OpenPypeVersion().get_remote_versions(*args, **kwargs)
return None
def get_latest_version(*args, **kwargs):
"""Get latest version from repository path."""
if op_version_control_available():
return get_OpenPypeVersion().get_latest_version(*args, **kwargs)
return None
def get_expected_studio_version(staging=False):
"""Expected production or staging version in studio."""
if op_version_control_available():
return get_OpenPypeVersion().get_expected_studio_version(staging)
return None

View file

@ -49,32 +49,30 @@ def modules_from_path(folder_path):
Arguments:
path (str): Path to folder containing python scripts.
return_crasher (bool): Crashed module paths with exception info
will be returned too.
Returns:
list, tuple: List of modules when `return_crashed` is False else tuple
with list of modules at first place and tuple of path and exception
info at second place.
tuple<list, list>: First list contains successfully imported modules
and second list contains tuples of path and exception.
"""
crashed = []
modules = []
output = (modules, crashed)
# Just skip and return empty list if path is not set
if not folder_path:
return modules
return output
# Do not allow relative imports
if folder_path.startswith("."):
log.warning((
"BUG: Relative paths are not allowed for security reasons. {}"
).format(folder_path))
return modules
return output
folder_path = os.path.normpath(folder_path)
if not os.path.isdir(folder_path):
log.warning("Not a directory path: {}".format(folder_path))
return modules
return output
for filename in os.listdir(folder_path):
# Ignore files which start with underscore
@ -101,7 +99,7 @@ def modules_from_path(folder_path):
)
continue
return modules, crashed
return output
def recursive_bases_from_class(klass):

View file

@ -1,6 +1,7 @@
import os
import json
import collections
import platform
import click
@ -42,18 +43,26 @@ class FtrackModule(
self.ftrack_url = ftrack_url
current_dir = os.path.dirname(os.path.abspath(__file__))
low_platform = platform.system().lower()
# Server event handler paths
server_event_handlers_paths = [
os.path.join(current_dir, "event_handlers_server")
]
server_event_handlers_paths.extend(
ftrack_settings["ftrack_events_path"]
)
settings_server_paths = ftrack_settings["ftrack_events_path"]
if isinstance(settings_server_paths, dict):
settings_server_paths = settings_server_paths[low_platform]
server_event_handlers_paths.extend(settings_server_paths)
# User event handler paths
user_event_handlers_paths = [
os.path.join(current_dir, "event_handlers_user")
]
user_event_handlers_paths.extend(
ftrack_settings["ftrack_actions_path"]
)
settings_action_paths = ftrack_settings["ftrack_actions_path"]
if isinstance(settings_action_paths, dict):
settings_action_paths = settings_action_paths[low_platform]
user_event_handlers_paths.extend(settings_action_paths)
# Prepare attribute
self.server_event_handlers_paths = server_event_handlers_paths
self.user_event_handlers_paths = user_event_handlers_paths

View file

@ -63,6 +63,12 @@ class FtrackServer:
# Iterate all paths
register_functions = []
for path in paths:
# Try to format path with environments
try:
path = path.format(**os.environ)
except BaseException:
pass
# Get all modules with functions
modules, crashed = modules_from_path(path)
for filepath, exc_info in crashed:

View file

@ -2,6 +2,8 @@
"studio_name": "Studio name",
"studio_code": "stu",
"admin_password": "",
"production_version": "",
"staging_version": "",
"environment": {
"__environment_keys__": {
"global": []

View file

@ -15,8 +15,16 @@
"ftrack": {
"enabled": true,
"ftrack_server": "",
"ftrack_actions_path": [],
"ftrack_events_path": [],
"ftrack_actions_path": {
"windows": [],
"darwin": [],
"linux": []
},
"ftrack_events_path": {
"windows": [],
"darwin": [],
"linux": []
},
"intent": {
"items": {
"-": "-",

View file

@ -57,7 +57,7 @@ from .exceptions import (
SchemaError,
DefaultsNotDefined,
StudioDefaultsNotDefined,
BaseInvalidValueType,
BaseInvalidValue,
InvalidValueType,
InvalidKeySymbols,
SchemaMissingFileInfo,
@ -106,7 +106,7 @@ from .enum_entity import (
ToolsEnumEntity,
TaskTypeEnumEntity,
DeadlineUrlEnumEntity,
AnatomyTemplatesEnumEntity
AnatomyTemplatesEnumEntity,
)
from .list_entity import ListEntity
@ -122,12 +122,15 @@ from .dict_conditional import (
)
from .anatomy_entities import AnatomyEntity
from .op_version_entity import (
ProductionVersionsInputEntity,
StagingVersionsInputEntity
)
__all__ = (
"DefaultsNotDefined",
"StudioDefaultsNotDefined",
"BaseInvalidValueType",
"BaseInvalidValue",
"InvalidValueType",
"InvalidKeySymbols",
"SchemaMissingFileInfo",
@ -181,5 +184,8 @@ __all__ = (
"DictConditionalEntity",
"SyncServerProviders",
"AnatomyEntity"
"AnatomyEntity",
"ProductionVersionsInputEntity",
"StagingVersionsInputEntity"
)

View file

@ -9,7 +9,7 @@ from .lib import (
)
from .exceptions import (
BaseInvalidValueType,
BaseInvalidValue,
InvalidValueType,
SchemeGroupHierarchyBug,
EntitySchemaError
@ -437,7 +437,7 @@ class BaseItemEntity(BaseEntity):
try:
new_value = self.convert_to_valid_type(value)
except BaseInvalidValueType:
except BaseInvalidValue:
new_value = NOT_SET
if new_value is not NOT_SET:

View file

@ -1,7 +1,7 @@
from .lib import STRING_TYPE
from .input_entities import InputEntity
from .exceptions import (
BaseInvalidValueType,
BaseInvalidValue,
InvalidValueType
)
@ -47,7 +47,7 @@ class ColorEntity(InputEntity):
reason = "Color entity expect 4 items in list got {}".format(
len(value)
)
raise BaseInvalidValueType(reason, self.path)
raise BaseInvalidValue(reason, self.path)
new_value = []
for item in value:
@ -60,7 +60,7 @@ class ColorEntity(InputEntity):
reason = (
"Color entity expect 4 integers in range 0-255 got {}"
).format(value)
raise BaseInvalidValueType(reason, self.path)
raise BaseInvalidValue(reason, self.path)
new_value.append(item)
# Make sure

View file

@ -15,14 +15,14 @@ class StudioDefaultsNotDefined(Exception):
super(StudioDefaultsNotDefined, self).__init__(msg)
class BaseInvalidValueType(Exception):
class BaseInvalidValue(Exception):
def __init__(self, reason, path):
msg = "Path \"{}\". {}".format(path, reason)
self.msg = msg
super(BaseInvalidValueType, self).__init__(msg)
super(BaseInvalidValue, self).__init__(msg)
class InvalidValueType(BaseInvalidValueType):
class InvalidValueType(BaseInvalidValue):
def __init__(self, valid_types, invalid_type, path):
joined_types = ", ".join(
[str(valid_type) for valid_type in valid_types]

View file

@ -441,6 +441,16 @@ class TextEntity(InputEntity):
# GUI attributes
self.multiline = self.schema_data.get("multiline", False)
self.placeholder_text = self.schema_data.get("placeholder")
self.value_hints = self.schema_data.get("value_hints") or []
def schema_validations(self):
if self.multiline and self.value_hints:
reason = (
"TextEntity entity can't use value hints"
" for multiline input (yet)."
)
raise EntitySchemaError(self, reason)
super(TextEntity, self).schema_validations()
def _convert_to_valid_type(self, value):
# Allow numbers converted to string

View file

@ -0,0 +1,89 @@
from openpype.lib.openpype_version import (
get_remote_versions,
get_OpenPypeVersion,
get_installed_version
)
from .input_entities import TextEntity
from .lib import (
OverrideState,
NOT_SET
)
from .exceptions import BaseInvalidValue
class OpenPypeVersionInput(TextEntity):
"""Entity to store OpenPype version to use.
Settings created on another machine may affect available versions
on current user's machine. Text input element is provided to explicitly
set version not yet showing up the user's machine.
It is possible to enter empty string. In that case is used any latest
version. Any other string must match regex of OpenPype version semantic.
"""
def _item_initialization(self):
super(OpenPypeVersionInput, self)._item_initialization()
self.multiline = False
self.placeholder_text = "Latest"
self.value_hints = []
def _get_openpype_versions(self):
"""This is abstract method returning version hints for UI purposes."""
raise NotImplementedError((
"{} does not have implemented '_get_openpype_versions'"
).format(self.__class__.__name__))
def set_override_state(self, state, *args, **kwargs):
"""Update value hints for UI purposes."""
value_hints = []
if state is OverrideState.STUDIO:
versions = self._get_openpype_versions()
for version in versions:
version_str = str(version)
if version_str not in value_hints:
value_hints.append(version_str)
self.value_hints = value_hints
super(OpenPypeVersionInput, self).set_override_state(
state, *args, **kwargs
)
def convert_to_valid_type(self, value):
"""Add validation of version regex."""
if value and value is not NOT_SET:
OpenPypeVersion = get_OpenPypeVersion()
if OpenPypeVersion is not None:
try:
OpenPypeVersion(version=value)
except Exception:
raise BaseInvalidValue(
"Value \"{}\"is not valid version format.".format(
value
),
self.path
)
return super(OpenPypeVersionInput, self).convert_to_valid_type(value)
class ProductionVersionsInputEntity(OpenPypeVersionInput):
"""Entity meant only for global settings to define production version."""
schema_types = ["production-versions-text"]
def _get_openpype_versions(self):
versions = get_remote_versions(staging=False, production=True)
if versions is None:
return []
versions.append(get_installed_version())
return sorted(versions)
class StagingVersionsInputEntity(OpenPypeVersionInput):
"""Entity meant only for global settings to define staging version."""
schema_types = ["staging-versions-text"]
def _get_openpype_versions(self):
versions = get_remote_versions(staging=True, production=False)
if versions is None:
return []
return sorted(versions)

View file

@ -21,19 +21,23 @@
},
{
"type": "label",
"label": "Additional Ftrack paths"
"label": "Additional Ftrack event handlers paths"
},
{
"type": "list",
"type": "path",
"key": "ftrack_actions_path",
"label": "Action paths",
"object_type": "text"
"label": "User paths",
"use_label_wrap": true,
"multipath": true,
"multiplatform": true
},
{
"type": "list",
"type": "path",
"key": "ftrack_events_path",
"label": "Event paths",
"object_type": "text"
"label": "Server paths",
"use_label_wrap": true,
"multipath": true,
"multiplatform": true
},
{
"type": "separator"

View file

@ -20,7 +20,7 @@
},
{
"type": "label",
"label": "This is <b>NOT a securely stored password!</b>. It only acts as a simple barrier to stop users from accessing studio wide settings."
"label": "This is <b>NOT a securely stored password!</b> It only acts as a simple barrier to stop users from accessing studio wide settings."
},
{
"type": "text",
@ -30,6 +30,23 @@
{
"type": "splitter"
},
{
"type": "label",
"label": "Define explicit OpenPype version that should be used. Keep empty to use latest available version."
},
{
"type": "production-versions-text",
"key": "production_version",
"label": "Production version"
},
{
"type": "staging-versions-text",
"key": "staging_version",
"label": "Staging version"
},
{
"type": "splitter"
},
{
"key": "environment",
"label": "Environment",

View file

@ -168,7 +168,13 @@ class CacheValues:
class MongoSettingsHandler(SettingsHandler):
"""Settings handler that use mongo for storing and loading of settings."""
global_general_keys = ("openpype_path", "admin_password", "disk_mapping")
global_general_keys = (
"openpype_path",
"admin_password",
"disk_mapping",
"production_version",
"staging_version"
)
def __init__(self):
# Get mongo connection

View file

@ -1,8 +1,10 @@
import os
import json
import collections
from openpype import resources
import six
from openpype import resources
from .color_defs import parse_color
@ -12,6 +14,18 @@ _FONT_IDS = None
current_dir = os.path.dirname(os.path.abspath(__file__))
def get_style_image_path(image_name):
# All filenames are lowered
image_name = image_name.lower()
# Male sure filename has png extension
if not image_name.endswith(".png"):
image_name += ".png"
filepath = os.path.join(current_dir, "images", image_name)
if os.path.exists(filepath):
return filepath
return None
def _get_colors_raw_data():
"""Read data file with stylesheet fill values.
@ -160,6 +174,11 @@ def load_stylesheet():
return _STYLESHEET_CACHE
def app_icon_path():
def get_app_icon_path():
"""Path to OpenPype icon."""
return resources.get_openpype_icon_filepath()
def app_icon_path():
# Backwards compatibility
return get_app_icon_path()

View file

@ -111,7 +111,9 @@
"focus-border": "#839caf",
"image-btn": "#bfccd6",
"image-btn-hover": "#189aea",
"image-btn-disabled": "#bfccd6"
"image-btn-disabled": "#bfccd6",
"version-exists": "#458056",
"version-not-found": "#ffc671"
}
}
}

View file

@ -536,6 +536,27 @@ QAbstractItemView::branch:!has-children:!has-siblings:adjoins-item {
background: transparent;
}
CompleterView {
border: 1px solid #555555;
background: {color:bg-inputs};
}
CompleterView::item:selected {
background: {color:bg-view-hover};
}
CompleterView::item:selected:hover {
background: {color:bg-view-hover};
}
CompleterView::right-arrow {
min-width: 10px;
}
CompleterView::separator {
background: {color:bg-menu-separator};
height: 2px;
margin-right: 5px;
}
/* Progress bar */
QProgressBar {
@ -1158,6 +1179,14 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
border-radius: 5px;
}
#OpenPypeVersionLabel[state="success"] {
color: {color:settings:version-exists};
}
#OpenPypeVersionLabel[state="warning"] {
color: {color:settings:version-not-found};
}
#ShadowWidget {
font-size: 36pt;
}

View file

@ -7,9 +7,10 @@ from avalon.vendor import qtawesome
from openpype import style
from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS
from openpype.tools.utils import ErrorMessageBox
class CreateErrorMessageBox(QtWidgets.QDialog):
class CreateErrorMessageBox(ErrorMessageBox):
def __init__(
self,
family,
@ -17,23 +18,38 @@ class CreateErrorMessageBox(QtWidgets.QDialog):
asset_name,
exc_msg,
formatted_traceback,
parent=None
parent
):
super(CreateErrorMessageBox, self).__init__(parent)
self.setWindowTitle("Creation failed")
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setWindowFlags(
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
)
self._family = family
self._subset_name = subset_name
self._asset_name = asset_name
self._exc_msg = exc_msg
self._formatted_traceback = formatted_traceback
super(CreateErrorMessageBox, self).__init__("Creation failed", parent)
body_layout = QtWidgets.QVBoxLayout(self)
main_label = (
def _create_top_widget(self, parent_widget):
label_widget = QtWidgets.QLabel(parent_widget)
label_widget.setText(
"<span style='font-size:18pt;'>Failed to create</span>"
)
main_label_widget = QtWidgets.QLabel(main_label, self)
body_layout.addWidget(main_label_widget)
return label_widget
def _get_report_data(self):
report_message = (
"Failed to create Subset: \"{subset}\" Family: \"{family}\""
" in Asset: \"{asset}\""
"\n\nError: {message}"
).format(
subset=self._subset_name,
family=self._family,
asset=self._asset_name,
message=self._exc_msg
)
if self._formatted_traceback:
report_message += "\n\n{}".format(self._formatted_traceback)
return [report_message]
def _create_content(self, content_layout):
item_name_template = (
"<span style='font-weight:bold;'>Family:</span> {}<br>"
"<span style='font-weight:bold;'>Subset:</span> {}<br>"
@ -42,50 +58,29 @@ class CreateErrorMessageBox(QtWidgets.QDialog):
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
line = self._create_line()
body_layout.addWidget(line)
content_layout.addWidget(line)
item_name = item_name_template.format(family, subset_name, asset_name)
item_name_widget = QtWidgets.QLabel(
item_name.replace("\n", "<br>"), self
)
body_layout.addWidget(item_name_widget)
exc_msg = exc_msg_template.format(exc_msg.replace("\n", "<br>"))
message_label_widget = QtWidgets.QLabel(exc_msg, self)
body_layout.addWidget(message_label_widget)
if formatted_traceback:
tb_widget = QtWidgets.QLabel(
formatted_traceback.replace("\n", "<br>"), self
item_name_widget = QtWidgets.QLabel(self)
item_name_widget.setText(
item_name_template.format(
self._family, self._subset_name, self._asset_name
)
tb_widget.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
body_layout.addWidget(tb_widget)
footer_widget = QtWidgets.QWidget(self)
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
button_box = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical)
button_box.setStandardButtons(
QtWidgets.QDialogButtonBox.StandardButton.Ok
)
button_box.accepted.connect(self._on_accept)
footer_layout.addWidget(button_box, alignment=QtCore.Qt.AlignRight)
body_layout.addWidget(footer_widget)
content_layout.addWidget(item_name_widget)
def showEvent(self, event):
self.setStyleSheet(style.load_stylesheet())
super(CreateErrorMessageBox, self).showEvent(event)
message_label_widget = QtWidgets.QLabel(self)
message_label_widget.setText(
exc_msg_template.format(self.convert_text_for_html(self._exc_msg))
)
content_layout.addWidget(message_label_widget)
def _on_accept(self):
self.close()
def _create_line(self):
line = QtWidgets.QFrame(self)
line.setFixedHeight(2)
line.setFrameShape(QtWidgets.QFrame.HLine)
line.setFrameShadow(QtWidgets.QFrame.Sunken)
return line
if self._formatted_traceback:
line_widget = self._create_line()
tb_widget = self._create_traceback_widget(
self._formatted_traceback
)
content_layout.addWidget(line_widget)
content_layout.addWidget(tb_widget)
class SubsetNameValidator(QtGui.QRegExpValidator):

View file

@ -445,7 +445,11 @@ class CreatorWindow(QtWidgets.QDialog):
if error_info:
box = CreateErrorMessageBox(
creator_plugin.family, subset_name, asset_name, *error_info
creator_plugin.family,
subset_name,
asset_name,
*error_info,
parent=self
)
box.show()
# Store dialog so is not garbage collected before is shown

View file

@ -11,7 +11,10 @@ from Qt import QtWidgets, QtCore, QtGui
from avalon import api, pipeline
from avalon.lib import HeroVersionType
from openpype.tools.utils import lib as tools_lib
from openpype.tools.utils import (
ErrorMessageBox,
lib as tools_lib
)
from openpype.tools.utils.delegates import (
VersionDelegate,
PrettyTimeDelegate
@ -64,20 +67,37 @@ class OverlayFrame(QtWidgets.QFrame):
self.label_widget.setText(label)
class LoadErrorMessageBox(QtWidgets.QDialog):
class LoadErrorMessageBox(ErrorMessageBox):
def __init__(self, messages, parent=None):
super(LoadErrorMessageBox, self).__init__(parent)
self.setWindowTitle("Loading failed")
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self._messages = messages
super(LoadErrorMessageBox, self).__init__("Loading failed", parent)
body_layout = QtWidgets.QVBoxLayout(self)
main_label = (
def _create_top_widget(self, parent_widget):
label_widget = QtWidgets.QLabel(parent_widget)
label_widget.setText(
"<span style='font-size:18pt;'>Failed to load items</span>"
)
main_label_widget = QtWidgets.QLabel(main_label, self)
body_layout.addWidget(main_label_widget)
return label_widget
def _get_report_data(self):
report_data = []
for exc_msg, tb_text, repre, subset, version in self._messages:
report_message = (
"During load error happened on Subset: \"{subset}\""
" Representation: \"{repre}\" Version: {version}"
"\n\nError message: {message}"
).format(
subset=subset,
repre=repre,
version=version,
message=exc_msg
)
if tb_text:
report_message += "\n\n{}".format(tb_text)
report_data.append(report_message)
return report_data
def _create_content(self, content_layout):
item_name_template = (
"<span style='font-weight:bold;'>Subset:</span> {}<br>"
"<span style='font-weight:bold;'>Version:</span> {}<br>"
@ -85,46 +105,27 @@ class LoadErrorMessageBox(QtWidgets.QDialog):
)
exc_msg_template = "<span style='font-weight:bold'>{}</span>"
for exc_msg, tb, repre, subset, version in messages:
for exc_msg, tb_text, repre, subset, version in self._messages:
line = self._create_line()
body_layout.addWidget(line)
content_layout.addWidget(line)
item_name = item_name_template.format(subset, version, repre)
item_name_widget = QtWidgets.QLabel(
item_name.replace("\n", "<br>"), self
)
body_layout.addWidget(item_name_widget)
item_name_widget.setWordWrap(True)
content_layout.addWidget(item_name_widget)
exc_msg = exc_msg_template.format(exc_msg.replace("\n", "<br>"))
message_label_widget = QtWidgets.QLabel(exc_msg, self)
body_layout.addWidget(message_label_widget)
message_label_widget.setWordWrap(True)
content_layout.addWidget(message_label_widget)
if tb:
tb_widget = QtWidgets.QLabel(tb.replace("\n", "<br>"), self)
tb_widget.setTextInteractionFlags(
QtCore.Qt.TextBrowserInteraction
)
body_layout.addWidget(tb_widget)
footer_widget = QtWidgets.QWidget(self)
footer_layout = QtWidgets.QHBoxLayout(footer_widget)
buttonBox = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical)
buttonBox.setStandardButtons(
QtWidgets.QDialogButtonBox.StandardButton.Ok
)
buttonBox.accepted.connect(self._on_accept)
footer_layout.addWidget(buttonBox, alignment=QtCore.Qt.AlignRight)
body_layout.addWidget(footer_widget)
def _on_accept(self):
self.close()
def _create_line(self):
line = QtWidgets.QFrame(self)
line.setFixedHeight(2)
line.setFrameShape(QtWidgets.QFrame.HLine)
line.setFrameShadow(QtWidgets.QFrame.Sunken)
return line
if tb_text:
line = self._create_line()
tb_widget = self._create_traceback_widget(tb_text, self)
content_layout.addWidget(line)
content_layout.addWidget(tb_widget)
class SubsetWidget(QtWidgets.QWidget):
@ -535,7 +536,7 @@ class SubsetWidget(QtWidgets.QWidget):
self.load_ended.emit()
if error_info:
box = LoadErrorMessageBox(error_info)
box = LoadErrorMessageBox(error_info, self)
box.show()
def selected_subsets(self, _groups=False, _merged=False, _other=True):
@ -1431,7 +1432,7 @@ class RepresentationWidget(QtWidgets.QWidget):
self.load_ended.emit()
if errors:
box = LoadErrorMessageBox(errors)
box = LoadErrorMessageBox(errors, self)
box.show()
def _get_optional_labels(self, loaders, selected_side):

View file

@ -32,6 +32,15 @@ class BaseWidget(QtWidgets.QWidget):
self.label_widget = None
self.create_ui()
@staticmethod
def set_style_property(obj, property_name, property_value):
"""Change QWidget property and polish it's style."""
if obj.property(property_name) == property_value:
return
obj.setProperty(property_name, property_value)
obj.style().polish(obj)
def scroll_to(self, widget):
self.category_widget.scroll_to(widget)

View file

@ -29,6 +29,9 @@ from openpype.settings.entities import (
StudioDefaultsNotDefined,
SchemaError
)
from openpype.settings.entities.op_version_entity import (
OpenPypeVersionInput
)
from openpype.settings import SaveWarningExc
from .widgets import ProjectListWidget
@ -47,6 +50,7 @@ from .item_widgets import (
BoolWidget,
DictImmutableKeysWidget,
TextWidget,
OpenPypeVersionText,
NumberWidget,
RawJsonWidget,
EnumeratorWidget,
@ -118,6 +122,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
elif isinstance(entity, BoolEntity):
return BoolWidget(*args)
elif isinstance(entity, OpenPypeVersionInput):
return OpenPypeVersionText(*args)
elif isinstance(entity, TextEntity):
return TextWidget(*args)

View file

@ -2,6 +2,10 @@ import json
from Qt import QtWidgets, QtCore, QtGui
from openpype.widgets.sliders import NiceSlider
from openpype.tools.settings import CHILD_OFFSET
from openpype.settings.entities.exceptions import BaseInvalidValue
from .widgets import (
ExpandingWidget,
NumberSpinBox,
@ -22,9 +26,6 @@ from .base import (
InputWidget
)
from openpype.widgets.sliders import NiceSlider
from openpype.tools.settings import CHILD_OFFSET
class DictImmutableKeysWidget(BaseWidget):
def create_ui(self):
@ -378,6 +379,16 @@ class TextWidget(InputWidget):
self.input_field.focused_in.connect(self._on_input_focus)
self.input_field.textChanged.connect(self._on_value_change)
self._refresh_completer()
def _refresh_completer(self):
# Multiline entity can't have completer
# - there is not space for this UI component
if self.entity.multiline:
return
self.input_field.update_completer_values(self.entity.value_hints)
def _on_input_focus(self):
self.focused_in()
@ -406,6 +417,86 @@ class TextWidget(InputWidget):
self.entity.set(self.input_value())
class OpenPypeVersionText(TextWidget):
def __init__(self, *args, **kwargs):
self._info_widget = None
super(OpenPypeVersionText, self).__init__(*args, **kwargs)
def create_ui(self):
super(OpenPypeVersionText, self).create_ui()
info_widget = QtWidgets.QLabel(self)
info_widget.setObjectName("OpenPypeVersionLabel")
self.content_layout.addWidget(info_widget, 1)
self._info_widget = info_widget
def _update_info_widget(self):
value = self.input_value()
message = ""
tooltip = ""
state = None
if self._is_invalid:
message = "Invalid OpenPype version format"
elif value == "":
message = "Use latest available version"
tooltip = (
"Latest version from OpenPype zip repository will be used"
)
elif value in self.entity.value_hints:
state = "success"
message = "Version {} will be used".format(value)
else:
state = "warning"
message = (
"Version {} not found in listed versions".format(value)
)
if self.entity.value_hints:
tooltip = "Listed versions: {}".format(", ".join(
['"{}"'.format(hint) for hint in self.entity.value_hints]
))
else:
tooltip = "No versions were listed"
self._info_widget.setText(message)
self._info_widget.setToolTip(tooltip)
self.set_style_property(self._info_widget, "state", state)
def set_entity_value(self):
super(OpenPypeVersionText, self).set_entity_value()
self._invalidate()
self._update_info_widget()
def _on_value_change_timer(self):
value = self.input_value()
self._invalidate()
if not self.is_invalid:
self.entity.set(value)
self.update_style()
else:
# Manually trigger hierachical style update
self.ignore_input_changes.set_ignore(True)
self.ignore_input_changes.set_ignore(False)
self._update_info_widget()
def _invalidate(self):
value = self.input_value()
try:
self.entity.convert_to_valid_type(value)
is_invalid = False
except BaseInvalidValue:
is_invalid = True
self._is_invalid = is_invalid
def _on_entity_change(self):
super(OpenPypeVersionText, self)._on_entity_change()
self._refresh_completer()
class NumberWidget(InputWidget):
_slider_widget = None

View file

@ -1,4 +1,5 @@
import os
import copy
from Qt import QtWidgets, QtCore, QtGui
from avalon.vendor import qtawesome
from avalon.mongodb import (
@ -25,13 +26,235 @@ from .constants import (
)
class CompleterFilter(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(CompleterFilter, self).__init__(*args, **kwargs)
self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._text_filter = ""
def set_text_filter(self, text):
if self._text_filter == text:
return
self._text_filter = text
self.invalidateFilter()
def filterAcceptsRow(self, row, parent_index):
if not self._text_filter:
return True
model = self.sourceModel()
index = model.index(row, self.filterKeyColumn(), parent_index)
value = index.data(QtCore.Qt.DisplayRole)
if self._text_filter in value:
if self._text_filter == value:
return False
return True
return False
class CompleterView(QtWidgets.QListView):
row_activated = QtCore.Signal(str)
def __init__(self, parent):
super(CompleterView, self).__init__(parent)
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint
| QtCore.Qt.Tool
)
delegate = QtWidgets.QStyledItemDelegate()
self.setItemDelegate(delegate)
model = QtGui.QStandardItemModel()
filter_model = CompleterFilter()
filter_model.setSourceModel(model)
self.setModel(filter_model)
# self.installEventFilter(parent)
self.clicked.connect(self._on_activated)
self._last_loaded_values = None
self._model = model
self._filter_model = filter_model
self._delegate = delegate
def _on_activated(self, index):
if index.isValid():
value = index.data(QtCore.Qt.DisplayRole)
self.row_activated.emit(value)
def set_text_filter(self, text):
self._filter_model.set_text_filter(text)
self._update_geo()
def sizeHint(self):
result = super(CompleterView, self).sizeHint()
if self._filter_model.rowCount() == 0:
result.setHeight(0)
return result
def showEvent(self, event):
super(CompleterView, self).showEvent(event)
self._update_geo()
def _update_geo(self):
size_hint = self.sizeHint()
self.resize(size_hint.width(), size_hint.height())
def update_values(self, values):
if not values:
values = []
if self._last_loaded_values == values:
return
self._last_loaded_values = copy.deepcopy(values)
root_item = self._model.invisibleRootItem()
existing_values = {}
for row in reversed(range(root_item.rowCount())):
child = root_item.child(row)
value = child.data(QtCore.Qt.DisplayRole)
if value not in values:
root_item.removeRows(child.row())
else:
existing_values[value] = child
for row, value in enumerate(values):
if value in existing_values:
item = existing_values[value]
if item.row() == row:
continue
else:
item = QtGui.QStandardItem(value)
item.setEditable(False)
root_item.setChild(row, item)
self._update_geo()
def _get_selected_row(self):
indexes = self.selectionModel().selectedIndexes()
if not indexes:
return -1
return indexes[0].row()
def _select_row(self, row):
index = self._filter_model.index(row, 0)
self.setCurrentIndex(index)
def move_up(self):
rows = self._filter_model.rowCount()
if rows == 0:
return
selected_row = self._get_selected_row()
if selected_row < 0:
new_row = rows - 1
else:
new_row = selected_row - 1
if new_row < 0:
new_row = rows - 1
if new_row != selected_row:
self._select_row(new_row)
def move_down(self):
rows = self._filter_model.rowCount()
if rows == 0:
return
selected_row = self._get_selected_row()
if selected_row < 0:
new_row = 0
else:
new_row = selected_row + 1
if new_row >= rows:
new_row = 0
if new_row != selected_row:
self._select_row(new_row)
def enter_pressed(self):
selected_row = self._get_selected_row()
if selected_row < 0:
return
index = self._filter_model.index(selected_row, 0)
self._on_activated(index)
class SettingsLineEdit(PlaceholderLineEdit):
focused_in = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(SettingsLineEdit, self).__init__(*args, **kwargs)
self._completer = None
self.textChanged.connect(self._on_text_change)
def _on_text_change(self, text):
if self._completer is not None:
self._completer.set_text_filter(text)
def _update_completer(self):
if self._completer is None or not self._completer.isVisible():
return
point = self.frameGeometry().bottomLeft()
new_point = self.mapToGlobal(point)
self._completer.move(new_point)
def focusInEvent(self, event):
super(SettingsLineEdit, self).focusInEvent(event)
self.focused_in.emit()
if self._completer is None:
return
self._completer.show()
self._update_completer()
def focusOutEvent(self, event):
super(SettingsLineEdit, self).focusOutEvent(event)
if self._completer is not None:
self._completer.hide()
def paintEvent(self, event):
super(SettingsLineEdit, self).paintEvent(event)
self._update_completer()
def update_completer_values(self, values):
if not values and self._completer is None:
return
self._create_completer()
self._completer.update_values(values)
def _create_completer(self):
if self._completer is None:
self._completer = CompleterView(self)
self._completer.row_activated.connect(self._completer_activated)
def _completer_activated(self, text):
self.setText(text)
def keyPressEvent(self, event):
if self._completer is None:
super(SettingsLineEdit, self).keyPressEvent(event)
return
key = event.key()
if key == QtCore.Qt.Key_Up:
self._completer.move_up()
elif key == QtCore.Qt.Key_Down:
self._completer.move_down()
elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
self._completer.enter_pressed()
else:
super(SettingsLineEdit, self).keyPressEvent(event)
class SettingsPlainTextEdit(QtWidgets.QPlainTextEdit):
focused_in = QtCore.Signal()

View file

@ -1,8 +1,18 @@
from .widgets import (
PlaceholderLineEdit,
BaseClickableFrame,
ClickableFrame,
ExpandBtn,
)
from .error_dialog import ErrorMessageBox
__all__ = (
"PlaceholderLineEdit",
"BaseClickableFrame",
"ClickableFrame",
"ExpandBtn",
"ErrorMessageBox"
)

View file

@ -0,0 +1,152 @@
from Qt import QtWidgets, QtCore
from .widgets import ClickableFrame, ExpandBtn
def convert_text_for_html(text):
return (
text
.replace("<", "&#60;")
.replace(">", "&#62;")
.replace("\n", "<br>")
.replace(" ", "&nbsp;")
)
class TracebackWidget(QtWidgets.QWidget):
def __init__(self, tb_text, parent):
super(TracebackWidget, self).__init__(parent)
# Modify text to match html
# - add more replacements when needed
tb_text = convert_text_for_html(tb_text)
expand_btn = ExpandBtn(self)
clickable_frame = ClickableFrame(self)
clickable_layout = QtWidgets.QHBoxLayout(clickable_frame)
clickable_layout.setContentsMargins(0, 0, 0, 0)
expand_label = QtWidgets.QLabel("Details", clickable_frame)
clickable_layout.addWidget(expand_label, 0)
clickable_layout.addStretch(1)
show_details_layout = QtWidgets.QHBoxLayout()
show_details_layout.addWidget(expand_btn, 0)
show_details_layout.addWidget(clickable_frame, 1)
text_widget = QtWidgets.QLabel(self)
text_widget.setText(tb_text)
text_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
text_widget.setVisible(False)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(show_details_layout, 0)
layout.addWidget(text_widget, 1)
clickable_frame.clicked.connect(self._on_show_details_click)
expand_btn.clicked.connect(self._on_show_details_click)
self._expand_btn = expand_btn
self._text_widget = text_widget
def _on_show_details_click(self):
self._text_widget.setVisible(not self._text_widget.isVisible())
self._expand_btn.set_collapsed(not self._text_widget.isVisible())
class ErrorMessageBox(QtWidgets.QDialog):
_default_width = 660
_default_height = 350
def __init__(self, title, parent):
super(ErrorMessageBox, self).__init__(parent)
self.setWindowTitle(title)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
top_widget = self._create_top_widget(self)
content_scroll = QtWidgets.QScrollArea(self)
content_scroll.setWidgetResizable(True)
content_widget = QtWidgets.QWidget(content_scroll)
content_scroll.setWidget(content_widget)
content_layout = QtWidgets.QVBoxLayout(content_widget)
content_layout.setContentsMargins(0, 0, 0, 0)
# Store content widget before creation of content
self._content_widget = content_widget
self._create_content(content_layout)
content_layout.addStretch(1)
copy_report_btn = QtWidgets.QPushButton("Copy report", self)
ok_btn = QtWidgets.QPushButton("OK", self)
footer_layout = QtWidgets.QHBoxLayout()
footer_layout.addWidget(copy_report_btn, 0)
footer_layout.addStretch(1)
footer_layout.addWidget(ok_btn, 0)
bottom_line = self._create_line()
body_layout = QtWidgets.QVBoxLayout(self)
body_layout.addWidget(top_widget, 0)
body_layout.addWidget(content_scroll, 1)
body_layout.addWidget(bottom_line, 0)
body_layout.addLayout(footer_layout, 0)
copy_report_btn.clicked.connect(self._on_copy_report)
ok_btn.clicked.connect(self._on_ok_clicked)
self.resize(self._default_width, self._default_height)
report_data = self._get_report_data()
if not report_data:
copy_report_btn.setVisible(False)
self._report_data = report_data
@staticmethod
def convert_text_for_html(text):
return convert_text_for_html(text)
def _create_top_widget(self, parent_widget):
label_widget = QtWidgets.QLabel(parent_widget)
label_widget.setText(
"<span style='font-size:18pt;'>Something went wrong</span>"
)
return label_widget
def _create_content(self, content_layout):
raise NotImplementedError(
"Method '_fill_content_layout' is not implemented!"
)
def _get_report_data(self):
return []
def _on_ok_clicked(self):
self.close()
def _on_copy_report(self):
report_text = (10 * "*").join(self._report_data)
mime_data = QtCore.QMimeData()
mime_data.setText(report_text)
QtWidgets.QApplication.instance().clipboard().setMimeData(
mime_data
)
def _create_line(self):
line = QtWidgets.QFrame(self)
line.setObjectName("Separator")
line.setMinimumHeight(2)
line.setMaximumHeight(2)
return line
def _create_traceback_widget(self, traceback_text, parent=None):
if parent is None:
parent = self._content_widget
return TracebackWidget(traceback_text, parent)

View file

@ -3,7 +3,10 @@ import logging
from Qt import QtWidgets, QtCore, QtGui
from avalon.vendor import qtawesome, qargparse
from openpype.style import get_objected_colors
from openpype.style import (
get_objected_colors,
get_style_image_path
)
log = logging.getLogger(__name__)
@ -25,6 +28,100 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit):
self.setPalette(filter_palette)
class BaseClickableFrame(QtWidgets.QFrame):
"""Widget that catch left mouse click and can trigger a callback.
Callback is defined by overriding `_mouse_release_callback`.
"""
def __init__(self, parent):
super(BaseClickableFrame, self).__init__(parent)
self._mouse_pressed = False
def _mouse_release_callback(self):
pass
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self._mouse_pressed = True
super(BaseClickableFrame, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
if self._mouse_pressed:
self._mouse_pressed = False
if self.rect().contains(event.pos()):
self._mouse_release_callback()
super(BaseClickableFrame, self).mouseReleaseEvent(event)
class ClickableFrame(BaseClickableFrame):
"""Extended clickable frame which triggers 'clicked' signal."""
clicked = QtCore.Signal()
def _mouse_release_callback(self):
self.clicked.emit()
class ExpandBtnLabel(QtWidgets.QLabel):
"""Label showing expand icon meant for ExpandBtn."""
def __init__(self, parent):
super(ExpandBtnLabel, self).__init__(parent)
self._source_collapsed_pix = QtGui.QPixmap(
get_style_image_path("branch_closed")
)
self._source_expanded_pix = QtGui.QPixmap(
get_style_image_path("branch_open")
)
self._current_image = self._source_collapsed_pix
self._collapsed = True
def set_collapsed(self, collapsed):
if self._collapsed == collapsed:
return
self._collapsed = collapsed
if collapsed:
self._current_image = self._source_collapsed_pix
else:
self._current_image = self._source_expanded_pix
self._set_resized_pix()
def resizeEvent(self, event):
self._set_resized_pix()
super(ExpandBtnLabel, self).resizeEvent(event)
def _set_resized_pix(self):
size = int(self.fontMetrics().height() / 2)
if size < 1:
size = 1
size += size % 2
self.setPixmap(
self._current_image.scaled(
size,
size,
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation
)
)
class ExpandBtn(ClickableFrame):
def __init__(self, parent=None):
super(ExpandBtn, self).__init__(parent)
pixmap_label = ExpandBtnLabel(self)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(pixmap_label)
self._pixmap_label = pixmap_label
def set_collapsed(self, collapsed):
self._pixmap_label.set_collapsed(collapsed)
class ImageButton(QtWidgets.QPushButton):
"""PushButton with icon and size of font.

View file

@ -1074,6 +1074,7 @@ class Window(QtWidgets.QMainWindow):
if "task" in context:
self.tasks_widget.select_task_name(context["task"])
self._on_task_changed()
def _on_asset_changed(self):
asset_id = self.assets_widget.get_selected_asset_id()

303
start.py
View file

@ -196,7 +196,8 @@ from igniter import BootstrapRepos # noqa: E402
from igniter.tools import (
get_openpype_global_settings,
get_openpype_path_from_db,
validate_mongo_connection
validate_mongo_connection,
OpenPypeVersionNotFound
) # noqa
from igniter.bootstrap_repos import OpenPypeVersion # noqa: E402
@ -467,23 +468,41 @@ def _process_arguments() -> tuple:
use_version = None
use_staging = False
commands = []
for arg in sys.argv:
if arg == "--use-version":
_print("!!! Please use option --use-version like:")
_print(" --use-version=3.0.0")
sys.exit(1)
if arg.startswith("--use-version="):
m = re.search(
r"--use-version=(?P<version>\d+\.\d+\.\d+(?:\S*)?)", arg)
if m and m.group('version'):
use_version = m.group('version')
_print(">>> Requested version [ {} ]".format(use_version))
sys.argv.remove(arg)
if "+staging" in use_version:
use_staging = True
break
# OpenPype version specification through arguments
use_version_arg = "--use-version"
for arg in sys.argv:
if arg.startswith(use_version_arg):
# Remove arg from sys argv
sys.argv.remove(arg)
# Extract string after use version arg
use_version_value = arg[len(use_version_arg):]
if (
not use_version_value
or not use_version_value.startswith("=")
):
_print("!!! Please use option --use-version like:")
_print(" --use-version=3.0.0")
sys.exit(1)
version_str = use_version_value[1:]
use_version = None
if version_str.lower() == "latest":
use_version = "latest"
else:
m = re.search(
r"(?P<version>\d+\.\d+\.\d+(?:\S*)?)", version_str
)
if m and m.group('version'):
use_version = m.group('version')
_print(">>> Requested version [ {} ]".format(use_version))
if "+staging" in use_version:
use_staging = True
break
if use_version is None:
_print("!!! Requested version isn't in correct format.")
_print((" Use --list-versions to find out"
" proper version string."))
@ -521,7 +540,7 @@ def _process_arguments() -> tuple:
if os.getenv("OPENPYPE_HEADLESS_MODE") == "1":
_print("!!! Cannot open Igniter dialog in headless mode.")
sys.exit(1)
import igniter
return_code = igniter.open_dialog()
# this is when we want to run OpenPype without installing anything.
@ -646,67 +665,62 @@ def _find_frozen_openpype(use_version: str = None,
(if requested).
"""
version_path = None
openpype_version = None
openpype_versions = bootstrap.find_openpype(include_zips=True,
staging=use_staging)
# Collect OpenPype versions
installed_version = OpenPypeVersion.get_installed_version()
# Expected version that should be used by studio settings
# - this option is used only if version is not explictly set and if
# studio has set explicit version in settings
studio_version = OpenPypeVersion.get_expected_studio_version(use_staging)
if use_version is not None:
# Specific version is defined
if use_version.lower() == "latest":
# Version says to use latest version
_print("Finding latest version defined by use version")
openpype_version = bootstrap.find_latest_openpype_version(
use_staging
)
else:
_print("Finding specified version \"{}\"".format(use_version))
openpype_version = bootstrap.find_openpype_version(
use_version, use_staging
)
if openpype_version is None:
raise OpenPypeVersionNotFound(
"Requested version \"{}\" was not found.".format(
use_version
)
)
elif studio_version is not None:
# Studio has defined a version to use
_print("Finding studio version \"{}\"".format(studio_version))
openpype_version = bootstrap.find_openpype_version(
studio_version, use_staging
)
if openpype_version is None:
raise OpenPypeVersionNotFound((
"Requested OpenPype version \"{}\" defined by settings"
" was not found."
).format(studio_version))
else:
# Default behavior to use latest version
_print("Finding latest version")
openpype_version = bootstrap.find_latest_openpype_version(
use_staging
)
if openpype_version is None:
if use_staging:
reason = "Didn't find any staging versions."
else:
reason = "Didn't find any versions."
raise OpenPypeVersionNotFound(reason)
# get local frozen version and add it to detected version so if it is
# newer it will be used instead.
local_version_str = bootstrap.get_version(
Path(os.environ["OPENPYPE_ROOT"]))
if local_version_str:
local_version = OpenPypeVersion(
version=local_version_str,
path=Path(os.environ["OPENPYPE_ROOT"]))
if local_version not in openpype_versions:
openpype_versions.append(local_version)
openpype_versions.sort()
# if latest is currently running, ditch whole list
# and run from current without installing it.
if local_version == openpype_versions[-1]:
os.environ["OPENPYPE_TRYOUT"] = "1"
openpype_versions = []
else:
_print("!!! Warning: cannot determine current running version.")
if not os.getenv("OPENPYPE_TRYOUT"):
try:
# use latest one found (last in the list is latest)
openpype_version = openpype_versions[-1]
except IndexError:
# no OpenPype version found, run Igniter and ask for them.
_print('*** No OpenPype versions found.')
if os.getenv("OPENPYPE_HEADLESS_MODE") == "1":
_print("!!! Cannot open Igniter dialog in headless mode.")
sys.exit(1)
_print("--- launching setup UI ...")
import igniter
return_code = igniter.open_dialog()
if return_code == 2:
os.environ["OPENPYPE_TRYOUT"] = "1"
if return_code == 3:
# run OpenPype after installation
_print('>>> Finding OpenPype again ...')
openpype_versions = bootstrap.find_openpype(
staging=use_staging)
try:
openpype_version = openpype_versions[-1]
except IndexError:
_print(("!!! Something is wrong and we didn't "
"found it again."))
sys.exit(1)
elif return_code != 2:
_print(f" . finished ({return_code})")
sys.exit(return_code)
if not openpype_versions:
# no openpype versions found anyway, lets use then the one
# shipped with frozen OpenPype
if not os.getenv("OPENPYPE_TRYOUT"):
_print("*** Still no luck finding OpenPype.")
_print(("*** We'll try to use the one coming "
"with OpenPype installation."))
if installed_version == openpype_version:
version_path = _bootstrap_from_code(use_version, use_staging)
openpype_version = OpenPypeVersion(
version=BootstrapRepos.get_version(version_path),
@ -714,22 +728,6 @@ def _find_frozen_openpype(use_version: str = None,
_initialize_environment(openpype_version)
return version_path
# get path of version specified in `--use-version`
local_version = bootstrap.get_version(OPENPYPE_ROOT)
if use_version and use_version != local_version:
# force the one user has selected
openpype_version = None
openpype_versions = bootstrap.find_openpype(include_zips=True,
staging=use_staging)
v: OpenPypeVersion
found = [v for v in openpype_versions if str(v) == use_version]
if found:
openpype_version = sorted(found)[-1]
if not openpype_version:
_print(f"!!! Requested version {use_version} was not found.")
list_versions(openpype_versions, local_version)
sys.exit(1)
# test if latest detected is installed (in user data dir)
is_inside = False
try:
@ -742,12 +740,12 @@ def _find_frozen_openpype(use_version: str = None,
if not is_inside:
# install latest version to user data dir
if os.getenv("OPENPYPE_HEADLESS_MODE", "0") != "1":
import igniter
version_path = igniter.open_update_window(openpype_version)
else:
if os.getenv("OPENPYPE_HEADLESS_MODE") == "1":
version_path = bootstrap.install_version(
openpype_version, force=True)
openpype_version, force=True
)
else:
version_path = igniter.open_update_window(openpype_version)
openpype_version.path = version_path
_initialize_environment(openpype_version)
@ -783,6 +781,13 @@ def _bootstrap_from_code(use_version, use_staging):
# run through repos and add them to `sys.path` and `PYTHONPATH`
# set root
_openpype_root = OPENPYPE_ROOT
# Unset use version if latest should be used
# - when executed from code then code is expected as latest
# - when executed from build then build is already marked as latest
# in '_find_frozen_openpype'
if use_version and use_version.lower() == "latest":
use_version = None
if getattr(sys, 'frozen', False):
local_version = bootstrap.get_version(Path(_openpype_root))
switch_str = f" - will switch to {use_version}" if use_version else ""
@ -790,47 +795,45 @@ def _bootstrap_from_code(use_version, use_staging):
assert local_version
else:
# get current version of OpenPype
local_version = bootstrap.get_local_live_version()
local_version = OpenPypeVersion.get_installed_version_str()
version_to_use = None
openpype_versions = bootstrap.find_openpype(
include_zips=True, staging=use_staging)
if use_staging and not use_version:
try:
version_to_use = openpype_versions[-1]
except IndexError:
_print("!!! No staging versions are found.")
list_versions(openpype_versions, local_version)
sys.exit(1)
# All cases when should be used different version than build
if (use_version and use_version != local_version) or use_staging:
if use_version:
# Explicit version should be used
version_to_use = bootstrap.find_openpype_version(
use_version, use_staging
)
if version_to_use is None:
raise OpenPypeVersionNotFound(
"Requested version \"{}\" was not found.".format(
use_version
)
)
else:
# Staging version should be used
version_to_use = bootstrap.find_latest_openpype_version(
use_staging
)
if version_to_use is None:
if use_staging:
reason = "Didn't find any staging versions."
else:
# This reason is backup for possible bug in code
reason = "Didn't find any versions."
raise OpenPypeVersionNotFound(reason)
# Start extraction of version if needed
if version_to_use.path.is_file():
version_to_use.path = bootstrap.extract_openpype(
version_to_use)
version_to_use.path = bootstrap.extract_openpype(version_to_use)
bootstrap.add_paths_from_directory(version_to_use.path)
os.environ["OPENPYPE_VERSION"] = str(version_to_use)
os.environ["OPENPYPE_VERSION"] = use_version
version_path = version_to_use.path
os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501
os.environ["OPENPYPE_REPOS_ROOT"] = (
version_path / "openpype"
).as_posix()
_openpype_root = version_to_use.path.as_posix()
elif use_version and use_version != local_version:
v: OpenPypeVersion
found = [v for v in openpype_versions if str(v) == use_version]
if found:
version_to_use = sorted(found)[-1]
if version_to_use:
# use specified
if version_to_use.path.is_file():
version_to_use.path = bootstrap.extract_openpype(
version_to_use)
bootstrap.add_paths_from_directory(version_to_use.path)
os.environ["OPENPYPE_VERSION"] = use_version
version_path = version_to_use.path
os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501
_openpype_root = version_to_use.path.as_posix()
else:
_print(f"!!! Requested version {use_version} was not found.")
list_versions(openpype_versions, local_version)
sys.exit(1)
else:
os.environ["OPENPYPE_VERSION"] = local_version
version_path = Path(_openpype_root)
@ -889,16 +892,6 @@ def boot():
# ------------------------------------------------------------------------
_startup_validations()
# ------------------------------------------------------------------------
# Play animation
# ------------------------------------------------------------------------
# from igniter.terminal_splash import play_animation
# don't play for silenced commands
# if all(item not in sys.argv for item in silent_commands):
# play_animation()
# ------------------------------------------------------------------------
# Process arguments
# ------------------------------------------------------------------------
@ -940,7 +933,7 @@ def boot():
if getattr(sys, 'frozen', False):
local_version = bootstrap.get_version(Path(OPENPYPE_ROOT))
else:
local_version = bootstrap.get_local_live_version()
local_version = OpenPypeVersion.get_installed_version_str()
if "validate" in commands:
_print(f">>> Validating version [ {use_version} ]")
@ -969,7 +962,6 @@ def boot():
)
sys.exit(1)
if not openpype_path:
_print("*** Cannot get OpenPype path from database.")
@ -989,7 +981,7 @@ def boot():
if getattr(sys, 'frozen', False):
local_version = bootstrap.get_version(Path(_openpype_root))
else:
local_version = bootstrap.get_local_live_version()
local_version = OpenPypeVersion.get_installed_version_str()
list_versions(openpype_versions, local_version)
sys.exit(1)
@ -1003,6 +995,15 @@ def boot():
# find versions of OpenPype to be used with frozen code
try:
version_path = _find_frozen_openpype(use_version, use_staging)
except OpenPypeVersionNotFound as exc:
message = str(exc)
_print(message)
if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1":
list_versions(openpype_versions, local_version)
else:
igniter.show_message_dialog("Version not found", message)
sys.exit(1)
except RuntimeError as e:
# no version to run
_print(f"!!! {e}")
@ -1015,7 +1016,17 @@ def boot():
sys.exit(1)
_print(f"--- version is valid")
else:
version_path = _bootstrap_from_code(use_version, use_staging)
try:
version_path = _bootstrap_from_code(use_version, use_staging)
except OpenPypeVersionNotFound as exc:
message = str(exc)
_print(message)
if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1":
list_versions(openpype_versions, local_version)
else:
igniter.show_message_dialog("Version not found", message)
sys.exit(1)
# set this to point either to `python` from venv in case of live code
# or to `openpype` or `openpype_console` in case of frozen code

View file

@ -140,9 +140,10 @@ def test_search_string_for_openpype_version(printer):
]
for ver_string in strings:
printer(f"testing {ver_string[0]} should be {ver_string[1]}")
assert OpenPypeVersion.version_in_str(ver_string[0]) == \
ver_string[1]
assert isinstance(
OpenPypeVersion.version_in_str(ver_string[0]),
OpenPypeVersion if ver_string[1] else type(None)
)
@pytest.mark.slow
def test_install_live_repos(fix_bootstrap, printer, monkeypatch, pytestconfig):