diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 95c8647d45..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "client/ayon_core/hosts/unreal/integration"] - path = client/ayon_core/hosts/unreal/integration - url = https://github.com/ynput/ayon-unreal-plugin.git diff --git a/client/ayon_core/addon/README.md b/client/ayon_core/addon/README.md index 88c27db154..e1c04ea0d6 100644 --- a/client/ayon_core/addon/README.md +++ b/client/ayon_core/addon/README.md @@ -35,14 +35,14 @@ AYON addons should contain separated logic of specific kind of implementation, s - addon has more logic when used in a tray - it is possible that addon can be used only in the tray - abstract methods - - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_addons` + - `tray_init` - initialization triggered after `initialize` when used in `TrayAddonsManager` and before `connect_with_addons` - `tray_menu` - add actions to tray widget's menu that represent the addon - `tray_start` - start of addon's login in tray - addon is initialized and connected with other addons - `tray_exit` - addon's cleanup like stop and join threads etc. - - order of calling is based on implementation this order is how it works with `TrayModulesManager` + - order of calling is based on implementation this order is how it works with `TrayAddonsManager` - it is recommended to import and use GUI implementation only in these methods -- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init` +- has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayAddonsManager` to True after `tray_init` - if addon has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations ### ITrayService diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index ccd73d456a..b9ecff4233 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -8,7 +8,6 @@ import inspect import logging import threading import collections - from uuid import uuid4 from abc import ABCMeta, abstractmethod @@ -29,41 +28,42 @@ from .interfaces import ( ) # Files that will be always ignored on addons import -IGNORED_FILENAMES = ( +IGNORED_FILENAMES = { "__pycache__", -) +} # Files ignored on addons import from "./ayon_core/modules" -IGNORED_DEFAULT_FILENAMES = ( +IGNORED_DEFAULT_FILENAMES = { "__init__.py", "base.py", "interfaces.py", "click_wrap.py", - "example_addons", - "default_modules", -) -IGNORED_HOSTS_IN_AYON = { - "flame", - "harmony", } -IGNORED_MODULES_IN_AYON = set() # When addon was moved from ayon-core codebase # - this is used to log the missing addon MOVED_ADDON_MILESTONE_VERSIONS = { + "aftereffects": VersionInfo(0, 2, 0), "applications": VersionInfo(0, 2, 0), + "blender": VersionInfo(0, 2, 0), "celaction": VersionInfo(0, 2, 0), "clockify": VersionInfo(0, 2, 0), + "deadline": VersionInfo(0, 2, 0), "flame": VersionInfo(0, 2, 0), "fusion": VersionInfo(0, 2, 0), + "harmony": VersionInfo(0, 2, 0), + "hiero": VersionInfo(0, 2, 0), "max": VersionInfo(0, 2, 0), "photoshop": VersionInfo(0, 2, 0), + "timers_manager": VersionInfo(0, 2, 0), "traypublisher": VersionInfo(0, 2, 0), "tvpaint": VersionInfo(0, 2, 0), "maya": VersionInfo(0, 2, 0), "nuke": VersionInfo(0, 2, 0), "resolve": VersionInfo(0, 2, 0), + "royalrender": VersionInfo(0, 2, 0), "substancepainter": VersionInfo(0, 2, 0), "houdini": VersionInfo(0, 3, 0), + "unreal": VersionInfo(0, 2, 0), } @@ -411,95 +411,59 @@ def _load_addons_in_core( ): # Add current directory at first place # - has small differences in import logic - hosts_dir = os.path.join(AYON_CORE_ROOT, "hosts") modules_dir = os.path.join(AYON_CORE_ROOT, "modules") + if not os.path.exists(modules_dir): + log.warning( + f"Could not find path when loading AYON addons \"{modules_dir}\"" + ) + return - ignored_host_names = set(IGNORED_HOSTS_IN_AYON) - ignored_module_dir_filenames = ( - set(IGNORED_DEFAULT_FILENAMES) - | IGNORED_MODULES_IN_AYON - ) + ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES - for dirpath in {hosts_dir, modules_dir}: - if not os.path.exists(dirpath): - log.warning(( - "Could not find path when loading AYON addons \"{}\"" - ).format(dirpath)) + for filename in os.listdir(modules_dir): + # Ignore filenames + if filename in ignored_filenames: continue - is_in_modules_dir = dirpath == modules_dir - if is_in_modules_dir: - ignored_filenames = ignored_module_dir_filenames - else: - ignored_filenames = ignored_host_names + fullpath = os.path.join(modules_dir, filename) + basename, ext = os.path.splitext(filename) - for filename in os.listdir(dirpath): - # Ignore filenames - if filename in IGNORED_FILENAMES or filename in ignored_filenames: + if basename in ignore_addon_names: + continue + + # Validations + if os.path.isdir(fullpath): + # Check existence of init file + init_path = os.path.join(fullpath, "__init__.py") + if not os.path.exists(init_path): + log.debug(( + "Addon directory does not contain __init__.py" + f" file {fullpath}" + )) continue - fullpath = os.path.join(dirpath, filename) - basename, ext = os.path.splitext(filename) + elif ext != ".py": + continue - if basename in ignore_addon_names: - continue + # TODO add more logic how to define if folder is addon or not + # - check manifest and content of manifest + try: + # Don't import dynamically current directory modules + new_import_str = f"{modules_key}.{basename}" - # Validations - if os.path.isdir(fullpath): - # Check existence of init file - init_path = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_path): - log.debug(( - "Addon directory does not contain __init__.py" - " file {}" - ).format(fullpath)) - continue + import_str = f"ayon_core.modules.{basename}" + default_module = __import__(import_str, fromlist=("", )) + sys.modules[new_import_str] = default_module + setattr(openpype_modules, basename, default_module) - elif ext not in (".py", ): - continue - - # TODO add more logic how to define if folder is addon or not - # - check manifest and content of manifest - try: - # Don't import dynamically current directory modules - new_import_str = "{}.{}".format(modules_key, basename) - if is_in_modules_dir: - import_str = "ayon_core.modules.{}".format(basename) - default_module = __import__(import_str, fromlist=("", )) - sys.modules[new_import_str] = default_module - setattr(openpype_modules, basename, default_module) - - else: - import_str = "ayon_core.hosts.{}".format(basename) - # Until all hosts are converted to be able use them as - # modules is this error check needed - try: - default_module = __import__( - import_str, fromlist=("", ) - ) - sys.modules[new_import_str] = default_module - setattr(openpype_modules, basename, default_module) - - except Exception: - log.warning( - "Failed to import host folder {}".format(basename), - exc_info=True - ) - - except Exception: - if is_in_modules_dir: - msg = "Failed to import in-core addon '{}'.".format( - basename - ) - else: - msg = "Failed to import addon '{}'.".format(fullpath) - log.error(msg, exc_info=True) + except Exception: + log.error( + f"Failed to import in-core addon '{basename}'.", + exc_info=True + ) def _load_addons(): - # Support to use 'openpype' imports - sys.modules["openpype"] = sys.modules["ayon_core"] - # Key under which will be modules imported in `sys.modules` modules_key = "openpype_modules" @@ -552,6 +516,9 @@ class AYONAddon(object): enabled = True _id = None + # Temporary variable for 'version' property + _missing_version_warned = False + def __init__(self, manager, settings): self.manager = manager @@ -582,6 +549,26 @@ class AYONAddon(object): pass + @property + def version(self): + """Addon version. + + Todo: + Should be abstract property (required). Introduced in + ayon-core 0.3.3 . + + Returns: + str: Addon version as semver compatible string. + + """ + if not self.__class__._missing_version_warned: + self.__class__._missing_version_warned = True + print( + f"DEV WARNING: Addon '{self.name}' does not have" + f" defined version." + ) + return "0.0.0" + def initialize(self, settings): """Initialization of addon attributes. @@ -697,6 +684,30 @@ class OpenPypeAddOn(OpenPypeModule): enabled = True +class _AddonReportInfo: + def __init__( + self, class_name, name, version, report_value_by_label + ): + self.class_name = class_name + self.name = name + self.version = version + self.report_value_by_label = report_value_by_label + + @classmethod + def from_addon(cls, addon, report): + class_name = addon.__class__.__name__ + report_value_by_label = { + label: reported.get(class_name) + for label, reported in report.items() + } + return cls( + addon.__class__.__name__, + addon.name, + addon.version, + report_value_by_label + ) + + class AddonsManager: """Manager of addons that helps to load and prepare them to work. @@ -873,10 +884,6 @@ class AddonsManager: name_alias = getattr(addon, "openpype_alias", None) if name_alias: aliased_names.append((name_alias, addon)) - enabled_str = "X" - if not addon.enabled: - enabled_str = " " - self.log.debug("[{}] {}".format(enabled_str, name)) now = time.time() report[addon.__class__.__name__] = now - prev_start_time @@ -888,6 +895,13 @@ class AddonsManager: exc_info=True ) + for addon_name in sorted(self._addons_by_name.keys()): + addon = self._addons_by_name[addon_name] + enabled_str = "X" if addon.enabled else " " + self.log.debug( + f"[{enabled_str}] {addon.name} ({addon.version})" + ) + for item in aliased_names: name_alias, addon = item if name_alias not in self._addons_by_name: @@ -1176,39 +1190,55 @@ class AddonsManager: available_col_names |= set(addon_names.keys()) # Prepare ordered dictionary for columns - cols = collections.OrderedDict() - # Add addon names to first columnt - cols["Addon name"] = list(sorted( - addon.__class__.__name__ + addons_info = [ + _AddonReportInfo.from_addon(addon, self._report) for addon in self.addons if addon.__class__.__name__ in available_col_names - )) + ] + addons_info.sort(key=lambda x: x.name) + + addon_name_rows = [ + addon_info.name + for addon_info in addons_info + ] + addon_version_rows = [ + addon_info.version + for addon_info in addons_info + ] + # Add total key (as last addon) - cols["Addon name"].append(self._report_total_key) + addon_name_rows.append(self._report_total_key) + addon_version_rows.append(f"({len(addons_info)})") + + cols = collections.OrderedDict() + # Add addon names to first columnt + cols["Addon name"] = addon_name_rows + cols["Version"] = addon_version_rows # Add columns from report + total_by_addon = { + row: 0 + for row in addon_name_rows + } for label in self._report.keys(): - cols[label] = [] - - total_addon_times = {} - for addon_name in cols["Addon name"]: - total_addon_times[addon_name] = 0 - - for label, reported in self._report.items(): - for addon_name in cols["Addon name"]: - col_time = reported.get(addon_name) - if col_time is None: - cols[label].append("N/A") + rows = [] + col_total = 0 + for addon_info in addons_info: + value = addon_info.report_value_by_label.get(label) + if value is None: + rows.append("N/A") continue - cols[label].append("{:.3f}".format(col_time)) - total_addon_times[addon_name] += col_time - + rows.append("{:.3f}".format(value)) + total_by_addon[addon_info.name] += value + col_total += value + total_by_addon[self._report_total_key] += col_total + rows.append("{:.3f}".format(col_total)) + cols[label] = rows # Add to also total column that should sum the row - cols[self._report_total_key] = [] - for addon_name in cols["Addon name"]: - cols[self._report_total_key].append( - "{:.3f}".format(total_addon_times[addon_name]) - ) + cols[self._report_total_key] = [ + "{:.3f}".format(total_by_addon[addon_name]) + for addon_name in cols["Addon name"] + ] # Prepare column widths and total row count # - column width is by diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index bd47dc1aac..60cf5624b0 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -268,7 +268,7 @@ def main(*args, **kwargs): os.path.join(AYON_CORE_ROOT, "tools"), # add common AYON vendor # (common for multiple Python interpreter versions) - os.path.join(AYON_CORE_ROOT, "vendor", "python", "common") + os.path.join(AYON_CORE_ROOT, "vendor", "python") ] for path in additional_paths: if path not in split_paths: diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 0fb18be687..35b7e294de 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -36,7 +36,7 @@ class Commands: log.warning( "Failed to add cli command for module \"{}\"".format( addon.name - ) + ), exc_info=True ) return click_func @@ -64,9 +64,10 @@ class Commands: get_global_context, ) - # Register target and host + import ayon_api import pyblish.util + # Register target and host if not isinstance(path, str): raise RuntimeError("Path to JSON must be a string.") @@ -86,6 +87,19 @@ class Commands: log = Logger.get_logger("CLI-publish") + # Make public ayon api behave as other user + # - this works only if public ayon api is using service user + username = os.environ.get("AYON_USERNAME") + if username: + # NOTE: ayon-python-api does not have public api function to find + # out if is used service user. So we need to have try > except + # block. + con = ayon_api.get_server_api_connection() + try: + con.set_default_service_username(username) + except ValueError: + pass + install_ayon_plugins() manager = AddonsManager() diff --git a/client/ayon_core/hosts/blender/__init__.py b/client/ayon_core/hosts/blender/__init__.py deleted file mode 100644 index 2a6603606a..0000000000 --- a/client/ayon_core/hosts/blender/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .addon import BlenderAddon - - -__all__ = ( - "BlenderAddon", -) diff --git a/client/ayon_core/hosts/hiero/__init__.py b/client/ayon_core/hosts/hiero/__init__.py deleted file mode 100644 index e6744d5aec..0000000000 --- a/client/ayon_core/hosts/hiero/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .addon import ( - HIERO_ROOT_DIR, - HieroAddon, -) - - -__all__ = ( - "HIERO_ROOT_DIR", - "HieroAddon", -) diff --git a/client/ayon_core/hosts/hiero/vendor/google/protobuf/util/__init__.py b/client/ayon_core/hosts/hiero/vendor/google/protobuf/util/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/hosts/unreal/README.md b/client/ayon_core/hosts/unreal/README.md deleted file mode 100644 index d131105659..0000000000 --- a/client/ayon_core/hosts/unreal/README.md +++ /dev/null @@ -1,9 +0,0 @@ -## Unreal Integration - -Supported Unreal Engine version is 4.26+ (mainly because of major Python changes done there). - -### Project naming -Unreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are -invalid. If Ayon detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` -will become `P123_myProject`. There is also soft-limit on project name length to be shorter than 20 characters. -Longer names will issue warning in Unreal Editor that there might be possible side effects. diff --git a/client/ayon_core/hosts/unreal/__init__.py b/client/ayon_core/hosts/unreal/__init__.py deleted file mode 100644 index 42dd8f0ac4..0000000000 --- a/client/ayon_core/hosts/unreal/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .addon import UnrealAddon - - -__all__ = ( - "UnrealAddon", -) diff --git a/client/ayon_core/hosts/unreal/addon.py b/client/ayon_core/hosts/unreal/addon.py deleted file mode 100644 index c65490bd8c..0000000000 --- a/client/ayon_core/hosts/unreal/addon.py +++ /dev/null @@ -1,74 +0,0 @@ -import os -import re -from ayon_core.addon import AYONAddon, IHostAddon - -UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) - - -class UnrealAddon(AYONAddon, IHostAddon): - name = "unreal" - host_name = "unreal" - - def get_global_environments(self): - return { - "AYON_UNREAL_ROOT": UNREAL_ROOT_DIR, - } - - def add_implementation_envs(self, env, app): - """Modify environments to contain all required for implementation.""" - # Set AYON_UNREAL_PLUGIN required for Unreal implementation - # Imports are in this method for Python 2 compatiblity of an addon - from pathlib import Path - - from .lib import get_compatible_integration - - from ayon_core.tools.utils import show_message_dialog - - pattern = re.compile(r'^\d+-\d+$') - - if not pattern.match(app.name): - msg = ( - "Unreal application key in the settings must be in format" - "'5-0' or '5-1'" - ) - show_message_dialog( - parent=None, - title="Unreal application name format", - message=msg, - level="critical") - raise ValueError(msg) - - ue_version = app.name.replace("-", ".") - unreal_plugin_path = os.path.join( - UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" - ) - if not Path(unreal_plugin_path).exists(): - compatible_versions = get_compatible_integration( - ue_version, Path(UNREAL_ROOT_DIR) / "integration" - ) - if compatible_versions: - unreal_plugin_path = compatible_versions[-1] / "Ayon" - unreal_plugin_path = unreal_plugin_path.as_posix() - - if not env.get("AYON_UNREAL_PLUGIN") or \ - env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: - env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path - - # Set default environments if are not set via settings - defaults = { - "AYON_LOG_NO_COLORS": "1", - "UE_PYTHONPATH": os.environ.get("PYTHONPATH", ""), - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value - - def get_launch_hook_paths(self, app): - if app.host_name != self.host_name: - return [] - return [ - os.path.join(UNREAL_ROOT_DIR, "hooks") - ] - - def get_workfile_extensions(self): - return [".uproject"] diff --git a/client/ayon_core/hosts/unreal/api/__init__.py b/client/ayon_core/hosts/unreal/api/__init__.py deleted file mode 100644 index 7e7f839f27..0000000000 --- a/client/ayon_core/hosts/unreal/api/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- -"""Unreal Editor Ayon host API.""" - -from .plugin import ( - UnrealActorCreator, - UnrealAssetCreator, - Loader -) - -from .pipeline import ( - install, - uninstall, - ls, - publish, - containerise, - show_creator, - show_loader, - show_publisher, - show_manager, - show_experimental_tools, - show_tools_dialog, - show_tools_popup, - instantiate, - UnrealHost, - set_sequence_hierarchy, - generate_sequence, - maintained_selection -) - -__all__ = [ - "UnrealActorCreator", - "UnrealAssetCreator", - "Loader", - "install", - "uninstall", - "ls", - "publish", - "containerise", - "show_creator", - "show_loader", - "show_publisher", - "show_manager", - "show_experimental_tools", - "show_tools_dialog", - "show_tools_popup", - "instantiate", - "UnrealHost", - "set_sequence_hierarchy", - "generate_sequence", - "maintained_selection" -] diff --git a/client/ayon_core/hosts/unreal/api/helpers.py b/client/ayon_core/hosts/unreal/api/helpers.py deleted file mode 100644 index e9ab3fb4c5..0000000000 --- a/client/ayon_core/hosts/unreal/api/helpers.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -import unreal # noqa - - -class AyonUnrealException(Exception): - pass - - -@unreal.uclass() -class AyonHelpers(unreal.AyonLib): - """Class wrapping some useful functions for Ayon. - - This class is extending native BP class in Ayon Integration Plugin. - - """ - - @unreal.ufunction(params=[str, unreal.LinearColor, bool]) - def set_folder_color(self, path: str, color: unreal.LinearColor) -> None: - """Set color on folder in Content Browser. - - This method sets color on folder in Content Browser. Unfortunately - there is no way to refresh Content Browser so new color isn't applied - immediately. They are saved to config file and appears correctly - only after Editor is restarted. - - Args: - path (str): Path to folder - color (:class:`unreal.LinearColor`): Color of the folder - - Example: - - AyonHelpers().set_folder_color( - "/Game/Path", unreal.LinearColor(a=1.0, r=1.0, g=0.5, b=0) - ) - - Note: - This will take effect only after Editor is restarted. I couldn't - find a way to refresh it. Also, this saves the color definition - into the project config, binding this path with color. So if you - delete this path and later re-create, it will set this color - again. - - """ - self.c_set_folder_color(path, color, False) diff --git a/client/ayon_core/hosts/unreal/api/pipeline.py b/client/ayon_core/hosts/unreal/api/pipeline.py deleted file mode 100644 index a60564d5b0..0000000000 --- a/client/ayon_core/hosts/unreal/api/pipeline.py +++ /dev/null @@ -1,804 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import json -import logging -from typing import List -from contextlib import contextmanager -import time - -import semver -import pyblish.api -import ayon_api - -from ayon_core.pipeline import ( - register_loader_plugin_path, - register_creator_plugin_path, - register_inventory_action_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, - deregister_inventory_action_path, - AYON_CONTAINER_ID, - get_current_project_name, -) -from ayon_core.tools.utils import host_tools -import ayon_core.hosts.unreal -from ayon_core.host import HostBase, ILoadHost, IPublishHost - -import unreal # noqa - -# Rename to Ayon once parent module renames -logger = logging.getLogger("ayon_core.hosts.unreal") - -AYON_CONTAINERS = "AyonContainers" -AYON_ASSET_DIR = "/Game/Ayon/Assets" -CONTEXT_CONTAINER = "Ayon/context.json" -UNREAL_VERSION = semver.VersionInfo( - *os.getenv("AYON_UNREAL_VERSION").split(".") -) - -HOST_DIR = os.path.dirname(os.path.abspath(ayon_core.hosts.unreal.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") - - -class UnrealHost(HostBase, ILoadHost, IPublishHost): - """Unreal host implementation. - - For some time this class will re-use functions from module based - implementation for backwards compatibility of older unreal projects. - """ - - name = "unreal" - - def install(self): - install() - - def get_containers(self): - return ls() - - @staticmethod - def show_tools_popup(): - """Show tools popup with actions leading to show other tools.""" - show_tools_popup() - - @staticmethod - def show_tools_dialog(): - """Show tools dialog with actions leading to show other tools.""" - show_tools_dialog() - - def update_context_data(self, data, changes): - content_path = unreal.Paths.project_content_dir() - op_ctx = content_path + CONTEXT_CONTAINER - attempts = 3 - for i in range(attempts): - try: - with open(op_ctx, "w+") as f: - json.dump(data, f) - break - except IOError as e: - if i == attempts - 1: - raise Exception( - "Failed to write context data. Aborting.") from e - unreal.log_warning("Failed to write context data. Retrying...") - i += 1 - time.sleep(3) - continue - - def get_context_data(self): - content_path = unreal.Paths.project_content_dir() - op_ctx = content_path + CONTEXT_CONTAINER - if not os.path.isfile(op_ctx): - return {} - with open(op_ctx, "r") as fp: - data = json.load(fp) - return data - - -def install(): - """Install Unreal configuration for AYON.""" - print("-=" * 40) - logo = '''. -. - · - │ - ·∙/ - ·-∙•∙-· - / \\ /∙· / \\ - ∙ \\ │ / ∙ - \\ \\ · / / - \\\\ ∙ ∙ // - \\\\/ \\// - ___ - │ │ - │ │ - │ │ - │___│ - -· - - ·-─═─-∙ A Y O N ∙-─═─-· - by YNPUT -. -''' - print(logo) - print("installing Ayon for Unreal ...") - print("-=" * 40) - logger.info("installing Ayon for Unreal") - pyblish.api.register_host("unreal") - pyblish.api.register_plugin_path(str(PUBLISH_PATH)) - register_loader_plugin_path(str(LOAD_PATH)) - register_creator_plugin_path(str(CREATE_PATH)) - register_inventory_action_path(str(INVENTORY_PATH)) - _register_callbacks() - _register_events() - - -def uninstall(): - """Uninstall Unreal configuration for Ayon.""" - pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) - deregister_loader_plugin_path(str(LOAD_PATH)) - deregister_creator_plugin_path(str(CREATE_PATH)) - deregister_inventory_action_path(str(INVENTORY_PATH)) - - -def _register_callbacks(): - """ - TODO: Implement callbacks if supported by UE - """ - pass - - -def _register_events(): - """ - TODO: Implement callbacks if supported by UE - """ - pass - - -def ls(): - """List all containers. - - List all found in *Content Manager* of Unreal and return - metadata from them. Adding `objectName` to set. - - """ - ar = unreal.AssetRegistryHelpers.get_asset_registry() - # UE 5.1 changed how class name is specified - class_name = ["/Script/Ayon", "AyonAssetContainer"] if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 else "AyonAssetContainer" # noqa - ayon_containers = ar.get_assets_by_class(class_name, True) - - # get_asset_by_class returns AssetData. To get all metadata we need to - # load asset. get_tag_values() work only on metadata registered in - # Asset Registry Project settings (and there is no way to set it with - # python short of editing ini configuration file). - for asset_data in ayon_containers: - asset = asset_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = asset_data.asset_name - yield cast_map_to_str_dict(data) - - -def ls_inst(): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - # UE 5.1 changed how class name is specified - class_name = [ - "/Script/Ayon", - "AyonPublishInstance" - ] if ( - UNREAL_VERSION.major == 5 - and UNREAL_VERSION.minor > 0 - ) else "AyonPublishInstance" # noqa - instances = ar.get_assets_by_class(class_name, True) - - # get_asset_by_class returns AssetData. To get all metadata we need to - # load asset. get_tag_values() work only on metadata registered in - # Asset Registry Project settings (and there is no way to set it with - # python short of editing ini configuration file). - for asset_data in instances: - asset = asset_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = asset_data.asset_name - yield cast_map_to_str_dict(data) - - -def parse_container(container): - """To get data from container, AyonAssetContainer must be loaded. - - Args: - container(str): path to container - - Returns: - dict: metadata stored on container - """ - asset = unreal.EditorAssetLibrary.load_asset(container) - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = asset.get_name() - data = cast_map_to_str_dict(data) - - return data - - -def publish(): - """Shorthand to publish from within host.""" - import pyblish.util - - return pyblish.util.publish() - - -def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): - """Bundles *nodes* (assets) into a *container* and add metadata to it. - - Unreal doesn't support *groups* of assets that you can add metadata to. - But it does support folders that helps to organize asset. Unfortunately - those folders are just that - you cannot add any additional information - to them. Ayon Integration Plugin is providing way out - Implementing - `AssetContainer` Blueprint class. This class when added to folder can - handle metadata on it using standard - :func:`unreal.EditorAssetLibrary.set_metadata_tag()` and - :func:`unreal.EditorAssetLibrary.get_metadata_tag_values()`. It also - stores and monitor all changes in assets in path where it resides. List of - those assets is available as `assets` property. - - This is list of strings starting with asset type and ending with its path: - `Material /Game/Ayon/Test/TestMaterial.TestMaterial` - - """ - # 1 - create directory for container - root = "/Game" - container_name = f"{name}{suffix}" - new_name = move_assets_to_path(root, container_name, nodes) - - # 2 - create Asset Container there - path = f"{root}/{new_name}" - create_container(container=container_name, path=path) - - namespace = path - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "name": new_name, - "namespace": namespace, - "loader": str(loader), - "representation": context["representation"]["id"], - } - # 3 - imprint data - imprint(f"{path}/{container_name}", data) - return path - - -def instantiate(root, name, data, assets=None, suffix="_INS"): - """Bundles *nodes* into *container*. - - Marking it with metadata as publishable instance. If assets are provided, - they are moved to new path where `AyonPublishInstance` class asset is - created and imprinted with metadata. - - This can then be collected for publishing by Pyblish for example. - - Args: - root (str): root path where to create instance container - name (str): name of the container - data (dict): data to imprint on container - assets (list of str): list of asset paths to include in publish - instance - suffix (str): suffix string to append to instance name - - """ - container_name = f"{name}{suffix}" - - # if we specify assets, create new folder and move them there. If not, - # just create empty folder - if assets: - new_name = move_assets_to_path(root, container_name, assets) - else: - new_name = create_folder(root, name) - - path = f"{root}/{new_name}" - create_publish_instance(instance=container_name, path=path) - - imprint(f"{path}/{container_name}", data) - - -def imprint(node, data): - loaded_asset = unreal.EditorAssetLibrary.load_asset(node) - for key, value in data.items(): - # Support values evaluated at imprint - if callable(value): - value = value() - # Unreal doesn't support NoneType in metadata values - if value is None: - value = "" - unreal.EditorAssetLibrary.set_metadata_tag( - loaded_asset, key, str(value) - ) - - with unreal.ScopedEditorTransaction("Ayon containerising"): - unreal.EditorAssetLibrary.save_asset(node) - - -def show_tools_popup(): - """Show popup with tools. - - Popup will disappear on click or losing focus. - """ - from ayon_core.hosts.unreal.api import tools_ui - - tools_ui.show_tools_popup() - - -def show_tools_dialog(): - """Show dialog with tools. - - Dialog will stay visible. - """ - from ayon_core.hosts.unreal.api import tools_ui - - tools_ui.show_tools_dialog() - - -def show_creator(): - host_tools.show_creator() - - -def show_loader(): - host_tools.show_loader(use_context=True) - - -def show_publisher(): - host_tools.show_publish() - - -def show_manager(): - host_tools.show_scene_inventory() - - -def show_experimental_tools(): - host_tools.show_experimental_tools_dialog() - - -def create_folder(root: str, name: str) -> str: - """Create new folder. - - If folder exists, append number at the end and try again, incrementing - if needed. - - Args: - root (str): path root - name (str): folder name - - Returns: - str: folder name - - Example: - >>> create_folder("/Game/Foo") - /Game/Foo - >>> create_folder("/Game/Foo") - /Game/Foo1 - - """ - eal = unreal.EditorAssetLibrary - index = 1 - while True: - if eal.does_directory_exist(f"{root}/{name}"): - name = f"{name}{index}" - index += 1 - else: - eal.make_directory(f"{root}/{name}") - break - - return name - - -def move_assets_to_path(root: str, name: str, assets: List[str]) -> str: - """Moving (renaming) list of asset paths to new destination. - - Args: - root (str): root of the path (eg. `/Game`) - name (str): name of destination directory (eg. `Foo` ) - assets (list of str): list of asset paths - - Returns: - str: folder name - - Example: - This will get paths of all assets under `/Game/Test` and move them - to `/Game/NewTest`. If `/Game/NewTest` already exists, then resulting - path will be `/Game/NewTest1` - - >>> assets = unreal.EditorAssetLibrary.list_assets("/Game/Test") - >>> move_assets_to_path("/Game", "NewTest", assets) - NewTest - - """ - eal = unreal.EditorAssetLibrary - name = create_folder(root, name) - - unreal.log(assets) - for asset in assets: - loaded = eal.load_asset(asset) - eal.rename_asset(asset, f"{root}/{name}/{loaded.get_name()}") - - return name - - -def create_container(container: str, path: str) -> unreal.Object: - """Helper function to create Asset Container class on given path. - - This Asset Class helps to mark given path as Container - and enable asset version control on it. - - Args: - container (str): Asset Container name - path (str): Path where to create Asset Container. This path should - point into container folder - - Returns: - :class:`unreal.Object`: instance of created asset - - Example: - - create_container( - "/Game/modelingFooCharacter_CON", - "modelingFooCharacter_CON" - ) - - """ - factory = unreal.AyonAssetContainerFactory() - tools = unreal.AssetToolsHelpers().get_asset_tools() - - return tools.create_asset(container, path, None, factory) - - -def create_publish_instance(instance: str, path: str) -> unreal.Object: - """Helper function to create Ayon Publish Instance on given path. - - This behaves similarly as :func:`create_ayon_container`. - - Args: - path (str): Path where to create Publish Instance. - This path should point into container folder - instance (str): Publish Instance name - - Returns: - :class:`unreal.Object`: instance of created asset - - Example: - - create_publish_instance( - "/Game/modelingFooCharacter_INST", - "modelingFooCharacter_INST" - ) - - """ - factory = unreal.AyonPublishInstanceFactory() - tools = unreal.AssetToolsHelpers().get_asset_tools() - return tools.create_asset(instance, path, None, factory) - - -def cast_map_to_str_dict(umap) -> dict: - """Cast Unreal Map to dict. - - Helper function to cast Unreal Map object to plain old python - dict. This will also cast values and keys to str. Useful for - metadata dicts. - - Args: - umap: Unreal Map object - - Returns: - dict - - """ - return {str(key): str(value) for (key, value) in umap.items()} - - -def get_subsequences(sequence: unreal.LevelSequence): - """Get list of subsequences from sequence. - - Args: - sequence (unreal.LevelSequence): Sequence - - Returns: - list(unreal.LevelSequence): List of subsequences - - """ - tracks = sequence.get_master_tracks() - subscene_track = next( - ( - t - for t in tracks - if t.get_class() == unreal.MovieSceneSubTrack.static_class() - ), - None, - ) - if subscene_track is not None and subscene_track.get_sections(): - return subscene_track.get_sections() - return [] - - -def set_sequence_hierarchy( - seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths -): - # Get existing sequencer tracks or create them if they don't exist - tracks = seq_i.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if not subscene_track: - subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - if not visibility_track: - visibility_track = seq_i.add_master_track( - unreal.MovieSceneLevelVisibilityTrack) - - # Create the sub-scene section - subscenes = subscene_track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = subscene_track.add_section() - subscene.set_row_index(len(subscene_track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - - # Create the visibility section - ar = unreal.AssetRegistryHelpers.get_asset_registry() - maps = [] - for m in map_paths: - # Unreal requires to load the level to get the map name - unreal.EditorLevelLibrary.save_all_dirty_levels() - unreal.EditorLevelLibrary.load_level(m) - maps.append(str(ar.get_asset_by_object_path(m).asset_name)) - - vis_section = visibility_track.add_section() - index = len(visibility_track.get_sections()) - - vis_section.set_range( - min_frame_j, - max_frame_j + 1) - vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) - vis_section.set_row_index(index) - vis_section.set_level_names(maps) - - if min_frame_j > 1: - hid_section = visibility_track.add_section() - hid_section.set_range( - 1, - min_frame_j) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - if max_frame_j < max_frame_i: - hid_section = visibility_track.add_section() - hid_section.set_range( - max_frame_j + 1, - max_frame_i + 1) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - - -def generate_sequence(h, h_dir): - tools = unreal.AssetToolsHelpers().get_asset_tools() - - sequence = tools.create_asset( - asset_name=h, - package_path=h_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - project_name = get_current_project_name() - # TODO Fix this does not return folder path - folder_path = h_dir.split('/')[-1], - folder_entity = ayon_api.get_folder_by_path( - project_name, - folder_path, - fields={"id", "attrib.fps"} - ) - - start_frames = [] - end_frames = [] - - elements = list(ayon_api.get_folders( - project_name, - parent_ids=[folder_entity["id"]], - fields={"id", "attrib.clipIn", "attrib.clipOut"} - )) - for e in elements: - start_frames.append(e["attrib"].get("clipIn")) - end_frames.append(e["attrib"].get("clipOut")) - - elements.extend(ayon_api.get_folders( - project_name, - parent_ids=[e["id"]], - fields={"id", "attrib.clipIn", "attrib.clipOut"} - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - fps = folder_entity["attrib"].get("fps") - - sequence.set_display_rate( - unreal.FrameRate(fps, 1.0)) - sequence.set_playback_start(min_frame) - sequence.set_playback_end(max_frame) - - sequence.set_work_range_start(min_frame / fps) - sequence.set_work_range_end(max_frame / fps) - sequence.set_view_range_start(min_frame / fps) - sequence.set_view_range_end(max_frame / fps) - - tracks = sequence.get_master_tracks() - track = None - for t in tracks: - if (t.get_class() == - unreal.MovieSceneCameraCutTrack.static_class()): - track = t - break - if not track: - track = sequence.add_master_track( - unreal.MovieSceneCameraCutTrack) - - return sequence, (min_frame, max_frame) - - -def _get_comps_and_assets( - component_class, asset_class, old_assets, new_assets, selected -): - eas = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) - - components = [] - if selected: - sel_actors = eas.get_selected_level_actors() - for actor in sel_actors: - comps = actor.get_components_by_class(component_class) - components.extend(comps) - else: - comps = eas.get_all_level_actors_components() - components = [ - c for c in comps if isinstance(c, component_class) - ] - - # Get all the static meshes among the old assets in a dictionary with - # the name as key - selected_old_assets = {} - for a in old_assets: - asset = unreal.EditorAssetLibrary.load_asset(a) - if isinstance(asset, asset_class): - selected_old_assets[asset.get_name()] = asset - - # Get all the static meshes among the new assets in a dictionary with - # the name as key - selected_new_assets = {} - for a in new_assets: - asset = unreal.EditorAssetLibrary.load_asset(a) - if isinstance(asset, asset_class): - selected_new_assets[asset.get_name()] = asset - - return components, selected_old_assets, selected_new_assets - - -def replace_static_mesh_actors(old_assets, new_assets, selected): - smes = unreal.get_editor_subsystem(unreal.StaticMeshEditorSubsystem) - - static_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets( - unreal.StaticMeshComponent, - unreal.StaticMesh, - old_assets, - new_assets, - selected - ) - - for old_name, old_mesh in old_meshes.items(): - new_mesh = new_meshes.get(old_name) - - if not new_mesh: - continue - - smes.replace_mesh_components_meshes( - static_mesh_comps, old_mesh, new_mesh) - - -def replace_skeletal_mesh_actors(old_assets, new_assets, selected): - skeletal_mesh_comps, old_meshes, new_meshes = _get_comps_and_assets( - unreal.SkeletalMeshComponent, - unreal.SkeletalMesh, - old_assets, - new_assets, - selected - ) - - for old_name, old_mesh in old_meshes.items(): - new_mesh = new_meshes.get(old_name) - - if not new_mesh: - continue - - for comp in skeletal_mesh_comps: - if comp.get_skeletal_mesh_asset() == old_mesh: - comp.set_skeletal_mesh_asset(new_mesh) - - -def replace_geometry_cache_actors(old_assets, new_assets, selected): - geometry_cache_comps, old_caches, new_caches = _get_comps_and_assets( - unreal.GeometryCacheComponent, - unreal.GeometryCache, - old_assets, - new_assets, - selected - ) - - for old_name, old_mesh in old_caches.items(): - new_mesh = new_caches.get(old_name) - - if not new_mesh: - continue - - for comp in geometry_cache_comps: - if comp.get_editor_property("geometry_cache") == old_mesh: - comp.set_geometry_cache(new_mesh) - - -def delete_asset_if_unused(container, asset_content): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - references = set() - - for asset_path in asset_content: - asset = ar.get_asset_by_object_path(asset_path) - refs = ar.get_referencers( - asset.package_name, - unreal.AssetRegistryDependencyOptions( - include_soft_package_references=False, - include_hard_package_references=True, - include_searchable_names=False, - include_soft_management_references=False, - include_hard_management_references=False - )) - if not refs: - continue - references = references.union(set(refs)) - - # Filter out references that are in the Temp folder - cleaned_references = { - ref for ref in references if not str(ref).startswith("/Temp/")} - - # Check which of the references are Levels - for ref in cleaned_references: - loaded_asset = unreal.EditorAssetLibrary.load_asset(ref) - if isinstance(loaded_asset, unreal.World): - # If there is at least a level, we can stop, we don't want to - # delete the container - return - - unreal.log("Previous version unused, deleting...") - - # No levels, delete the asset - unreal.EditorAssetLibrary.delete_directory(container["namespace"]) - - -@contextmanager -def maintained_selection(): - """Stub to be either implemented or replaced. - - This is needed for old publisher implementation, but - it is not supported (yet) in UE. - """ - try: - yield - finally: - pass diff --git a/client/ayon_core/hosts/unreal/api/plugin.py b/client/ayon_core/hosts/unreal/api/plugin.py deleted file mode 100644 index f31c7c46b9..0000000000 --- a/client/ayon_core/hosts/unreal/api/plugin.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- -import ast -import collections -import sys -import six -from abc import ( - ABC, - ABCMeta, -) - -import unreal - -from .pipeline import ( - create_publish_instance, - imprint, - ls_inst, - UNREAL_VERSION -) -from ayon_core.lib import ( - BoolDef, - UILabelDef -) -from ayon_core.pipeline import ( - Creator, - LoaderPlugin, - CreatorError, - CreatedInstance -) - - -@six.add_metaclass(ABCMeta) -class UnrealBaseCreator(Creator): - """Base class for Unreal creator plugins.""" - root = "/Game/Ayon/AyonPublishInstances" - suffix = "_INS" - - @staticmethod - def cache_instance_data(shared_data): - """Cache instances for Creators to shared data. - - Create `unreal_cached_instances` key when needed in shared data and - fill it with all collected instances from the scene under its - respective creator identifiers. - - If legacy instances are detected in the scene, create - `unreal_cached_legacy_instances` there and fill it with - all legacy products under family as a key. - - Args: - Dict[str, Any]: Shared data. - - """ - if "unreal_cached_instances" in shared_data: - return - - unreal_cached_instances = collections.defaultdict(list) - unreal_cached_legacy_instances = collections.defaultdict(list) - for instance in ls_inst(): - creator_id = instance.get("creator_identifier") - if creator_id: - unreal_cached_instances[creator_id].append(instance) - else: - family = instance.get("family") - unreal_cached_legacy_instances[family].append(instance) - - shared_data["unreal_cached_instances"] = unreal_cached_instances - shared_data["unreal_cached_legacy_instances"] = ( - unreal_cached_legacy_instances - ) - - def create(self, product_name, instance_data, pre_create_data): - try: - instance_name = f"{product_name}{self.suffix}" - pub_instance = create_publish_instance(instance_name, self.root) - - instance_data["productName"] = product_name - instance_data["instance_path"] = f"{self.root}/{instance_name}" - - instance = CreatedInstance( - self.product_type, - product_name, - instance_data, - self) - self._add_instance_to_context(instance) - - pub_instance.set_editor_property('add_external_assets', True) - assets = pub_instance.get_editor_property('asset_data_external') - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - for member in pre_create_data.get("members", []): - obj = ar.get_asset_by_object_path(member).get_asset() - assets.add(obj) - - imprint(f"{self.root}/{instance_name}", instance.data_to_store()) - - return instance - - except Exception as er: - six.reraise( - CreatorError, - CreatorError(f"Creator error: {er}"), - sys.exc_info()[2]) - - def collect_instances(self): - # cache instances if missing - self.cache_instance_data(self.collection_shared_data) - for instance in self.collection_shared_data[ - "unreal_cached_instances"].get(self.identifier, []): - # Unreal saves metadata as string, so we need to convert it back - instance['creator_attributes'] = ast.literal_eval( - instance.get('creator_attributes', '{}')) - instance['publish_attributes'] = ast.literal_eval( - instance.get('publish_attributes', '{}')) - created_instance = CreatedInstance.from_existing(instance, self) - self._add_instance_to_context(created_instance) - - def update_instances(self, update_list): - for created_inst, changes in update_list: - instance_node = created_inst.get("instance_path", "") - - if not instance_node: - unreal.log_warning( - f"Instance node not found for {created_inst}") - continue - - new_values = { - key: changes[key].new_value - for key in changes.changed_keys - } - imprint( - instance_node, - new_values - ) - - def remove_instances(self, instances): - for instance in instances: - instance_node = instance.data.get("instance_path", "") - if instance_node: - unreal.EditorAssetLibrary.delete_asset(instance_node) - - self._remove_instance_from_context(instance) - - -@six.add_metaclass(ABCMeta) -class UnrealAssetCreator(UnrealBaseCreator): - """Base class for Unreal creator plugins based on assets.""" - - def create(self, product_name, instance_data, pre_create_data): - """Create instance of the asset. - - Args: - product_name (str): Name of the product. - instance_data (dict): Data for the instance. - pre_create_data (dict): Data for the instance. - - Returns: - CreatedInstance: Created instance. - """ - try: - # Check if instance data has members, filled by the plugin. - # If not, use selection. - if not pre_create_data.get("members"): - pre_create_data["members"] = [] - - if pre_create_data.get("use_selection"): - utilib = unreal.EditorUtilityLibrary - sel_objects = utilib.get_selected_assets() - pre_create_data["members"] = [ - a.get_path_name() for a in sel_objects] - - super(UnrealAssetCreator, self).create( - product_name, - instance_data, - pre_create_data) - - except Exception as er: - six.reraise( - CreatorError, - CreatorError(f"Creator error: {er}"), - sys.exc_info()[2]) - - def get_pre_create_attr_defs(self): - return [ - BoolDef("use_selection", label="Use selection", default=True) - ] - - -@six.add_metaclass(ABCMeta) -class UnrealActorCreator(UnrealBaseCreator): - """Base class for Unreal creator plugins based on actors.""" - - def create(self, product_name, instance_data, pre_create_data): - """Create instance of the asset. - - Args: - product_name (str): Name of the product. - instance_data (dict): Data for the instance. - pre_create_data (dict): Data for the instance. - - Returns: - CreatedInstance: Created instance. - """ - try: - if UNREAL_VERSION.major == 5: - world = unreal.UnrealEditorSubsystem().get_editor_world() - else: - world = unreal.EditorLevelLibrary.get_editor_world() - - # Check if the level is saved - if world.get_path_name().startswith("/Temp/"): - raise CreatorError( - "Level must be saved before creating instances.") - - # Check if instance data has members, filled by the plugin. - # If not, use selection. - if not instance_data.get("members"): - actor_subsystem = unreal.EditorActorSubsystem() - sel_actors = actor_subsystem.get_selected_level_actors() - selection = [a.get_path_name() for a in sel_actors] - - instance_data["members"] = selection - - instance_data["level"] = world.get_path_name() - - super(UnrealActorCreator, self).create( - product_name, - instance_data, - pre_create_data) - - except Exception as er: - six.reraise( - CreatorError, - CreatorError(f"Creator error: {er}"), - sys.exc_info()[2]) - - def get_pre_create_attr_defs(self): - return [ - UILabelDef("Select actors to create instance from them.") - ] - - -class Loader(LoaderPlugin, ABC): - """This serves as skeleton for future Ayon specific functionality""" - pass diff --git a/client/ayon_core/hosts/unreal/api/rendering.py b/client/ayon_core/hosts/unreal/api/rendering.py deleted file mode 100644 index 395513aefa..0000000000 --- a/client/ayon_core/hosts/unreal/api/rendering.py +++ /dev/null @@ -1,180 +0,0 @@ -import os - -import unreal - -from ayon_core.settings import get_project_settings -from ayon_core.pipeline import Anatomy -from ayon_core.hosts.unreal.api import pipeline -from ayon_core.tools.utils import show_message_dialog - - -queue = None -executor = None - - -def _queue_finish_callback(exec, success): - unreal.log("Render completed. Success: " + str(success)) - - # Delete our reference so we don't keep it alive. - global executor - global queue - del executor - del queue - - -def _job_finish_callback(job, success): - # You can make any edits you want to the editor world here, and the world - # will be duplicated when the next render happens. Make sure you undo your - # edits in OnQueueFinishedCallback if you don't want to leak state changes - # into the editor world. - unreal.log("Individual job completed.") - - -def start_rendering(): - """ - Start the rendering process. - """ - unreal.log("Starting rendering...") - - # Get selected sequences - assets = unreal.EditorUtilityLibrary.get_selected_assets() - - if not assets: - show_message_dialog( - title="No assets selected", - message="No assets selected. Select a render instance.", - level="warning") - raise RuntimeError( - "No assets selected. You need to select a render instance.") - - # instances = pipeline.ls_inst() - instances = [ - a for a in assets - if a.get_class().get_name() == "AyonPublishInstance"] - - inst_data = [] - - for i in instances: - data = pipeline.parse_container(i.get_path_name()) - if data["productType"] == "render": - inst_data.append(data) - - try: - project = os.environ.get("AYON_PROJECT_NAME") - anatomy = Anatomy(project) - root = anatomy.roots['renders'] - except Exception as e: - raise Exception( - "Could not find render root in anatomy settings.") from e - - render_dir = f"{root}/{project}" - - # subsystem = unreal.get_editor_subsystem( - # unreal.MoviePipelineQueueSubsystem) - # queue = subsystem.get_queue() - global queue - queue = unreal.MoviePipelineQueue() - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - data = get_project_settings(project) - config = None - config_path = str(data.get("unreal").get("render_config_path")) - if config_path and unreal.EditorAssetLibrary.does_asset_exist(config_path): - unreal.log("Found saved render configuration") - config = ar.get_asset_by_object_path(config_path).get_asset() - - for i in inst_data: - sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() - - sequences = [{ - "sequence": sequence, - "output": f"{i['output']}", - "frame_range": ( - int(float(i["frameStart"])), - int(float(i["frameEnd"])) + 1) - }] - render_list = [] - - # Get all the sequences to render. If there are subsequences, - # add them and their frame ranges to the render list. We also - # use the names for the output paths. - for seq in sequences: - subscenes = pipeline.get_subsequences(seq.get('sequence')) - - if subscenes: - for sub_seq in subscenes: - sequences.append({ - "sequence": sub_seq.get_sequence(), - "output": (f"{seq.get('output')}/" - f"{sub_seq.get_sequence().get_name()}"), - "frame_range": ( - sub_seq.get_start_frame(), sub_seq.get_end_frame()) - }) - else: - # Avoid rendering camera sequences - if "_camera" not in seq.get('sequence').get_name(): - render_list.append(seq) - - # Create the rendering jobs and add them to the queue. - for render_setting in render_list: - job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) - job.sequence = unreal.SoftObjectPath(i["master_sequence"]) - job.map = unreal.SoftObjectPath(i["master_level"]) - job.author = "Ayon" - - # If we have a saved configuration, copy it to the job. - if config: - job.get_configuration().copy_from(config) - - # User data could be used to pass data to the job, that can be - # read in the job's OnJobFinished callback. We could, - # for instance, pass the AyonPublishInstance's path to the job. - # job.user_data = "" - - output_dir = render_setting.get('output') - shot_name = render_setting.get('sequence').get_name() - - settings = job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineOutputSetting) - settings.output_resolution = unreal.IntPoint(1920, 1080) - settings.custom_start_frame = render_setting.get("frame_range")[0] - settings.custom_end_frame = render_setting.get("frame_range")[1] - settings.use_custom_playback_range = True - settings.file_name_format = f"{shot_name}" + ".{frame_number}" - settings.output_directory.path = f"{render_dir}/{output_dir}" - - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineDeferredPassBase) - - render_format = data.get("unreal").get("render_format", "png") - - if render_format == "png": - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_PNG) - elif render_format == "exr": - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_EXR) - elif render_format == "jpg": - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_JPG) - elif render_format == "bmp": - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_BMP) - - # If there are jobs in the queue, start the rendering process. - if queue.get_jobs(): - global executor - executor = unreal.MoviePipelinePIEExecutor() - - preroll_frames = data.get("unreal").get("preroll_frames", 0) - - settings = unreal.MoviePipelinePIEExecutorSettings() - settings.set_editor_property( - "initial_delay_frame_count", preroll_frames) - - executor.on_executor_finished_delegate.add_callable_unique( - _queue_finish_callback) - executor.on_individual_job_finished_delegate.add_callable_unique( - _job_finish_callback) # Only available on PIE Executor - executor.execute(queue) diff --git a/client/ayon_core/hosts/unreal/api/tools_ui.py b/client/ayon_core/hosts/unreal/api/tools_ui.py deleted file mode 100644 index efae5bb702..0000000000 --- a/client/ayon_core/hosts/unreal/api/tools_ui.py +++ /dev/null @@ -1,162 +0,0 @@ -import sys -from qtpy import QtWidgets, QtCore, QtGui - -from ayon_core import ( - resources, - style -) -from ayon_core.tools.utils import host_tools -from ayon_core.tools.utils.lib import qt_app_context -from ayon_core.hosts.unreal.api import rendering - - -class ToolsBtnsWidget(QtWidgets.QWidget): - """Widget containing buttons which are clickable.""" - tool_required = QtCore.Signal(str) - - def __init__(self, parent=None): - super(ToolsBtnsWidget, self).__init__(parent) - - load_btn = QtWidgets.QPushButton("Load...", self) - publish_btn = QtWidgets.QPushButton("Publisher...", self) - manage_btn = QtWidgets.QPushButton("Manage...", self) - render_btn = QtWidgets.QPushButton("Render...", self) - experimental_tools_btn = QtWidgets.QPushButton( - "Experimental tools...", self - ) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(load_btn, 0) - layout.addWidget(publish_btn, 0) - layout.addWidget(manage_btn, 0) - layout.addWidget(render_btn, 0) - layout.addWidget(experimental_tools_btn, 0) - layout.addStretch(1) - - load_btn.clicked.connect(self._on_load) - publish_btn.clicked.connect(self._on_publish) - manage_btn.clicked.connect(self._on_manage) - render_btn.clicked.connect(self._on_render) - experimental_tools_btn.clicked.connect(self._on_experimental) - - def _on_create(self): - self.tool_required.emit("creator") - - def _on_load(self): - self.tool_required.emit("loader") - - def _on_publish(self): - self.tool_required.emit("publisher") - - def _on_manage(self): - self.tool_required.emit("sceneinventory") - - def _on_render(self): - rendering.start_rendering() - - def _on_experimental(self): - self.tool_required.emit("experimental_tools") - - -class ToolsDialog(QtWidgets.QDialog): - """Dialog with tool buttons that will stay opened until user close it.""" - def __init__(self, *args, **kwargs): - super(ToolsDialog, self).__init__(*args, **kwargs) - - self.setWindowTitle("Ayon tools") - icon = QtGui.QIcon(resources.get_ayon_icon_filepath()) - self.setWindowIcon(icon) - - self.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.WindowStaysOnTopHint - ) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - tools_widget = ToolsBtnsWidget(self) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(tools_widget) - - tools_widget.tool_required.connect(self._on_tool_require) - self._tools_widget = tools_widget - - self._first_show = True - - def sizeHint(self): - result = super(ToolsDialog, self).sizeHint() - result.setWidth(result.width() * 2) - return result - - def showEvent(self, event): - super(ToolsDialog, self).showEvent(event) - if self._first_show: - self.setStyleSheet(style.load_stylesheet()) - self._first_show = False - - def _on_tool_require(self, tool_name): - host_tools.show_tool_by_name(tool_name, parent=self) - - -class ToolsPopup(ToolsDialog): - """Popup with tool buttons that will close when loose focus.""" - def __init__(self, *args, **kwargs): - super(ToolsPopup, self).__init__(*args, **kwargs) - - self.setWindowFlags( - QtCore.Qt.FramelessWindowHint - | QtCore.Qt.Popup - ) - - def showEvent(self, event): - super(ToolsPopup, self).showEvent(event) - app = QtWidgets.QApplication.instance() - app.processEvents() - pos = QtGui.QCursor.pos() - self.move(pos) - - -class WindowCache: - """Cached objects and methods to be used in global scope.""" - _dialog = None - _popup = None - _first_show = True - - @classmethod - def _before_show(cls): - """Create QApplication if does not exist yet.""" - if not cls._first_show: - return - - cls._first_show = False - if not QtWidgets.QApplication.instance(): - QtWidgets.QApplication(sys.argv) - - @classmethod - def show_popup(cls): - cls._before_show() - with qt_app_context(): - if cls._popup is None: - cls._popup = ToolsPopup() - - cls._popup.show() - - @classmethod - def show_dialog(cls): - cls._before_show() - with qt_app_context(): - if cls._dialog is None: - cls._dialog = ToolsDialog() - - cls._dialog.show() - cls._dialog.raise_() - cls._dialog.activateWindow() - - -def show_tools_popup(): - WindowCache.show_popup() - - -def show_tools_dialog(): - WindowCache.show_dialog() diff --git a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py deleted file mode 100644 index e38591f65d..0000000000 --- a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py +++ /dev/null @@ -1,253 +0,0 @@ -# -*- coding: utf-8 -*- -"""Hook to launch Unreal and prepare projects.""" -import os -import copy -import shutil -import tempfile -from pathlib import Path - -from qtpy import QtCore - -from ayon_core import resources -from ayon_applications import ( - PreLaunchHook, - ApplicationLaunchFailed, - LaunchTypes, -) -from ayon_core.pipeline.workfile import get_workfile_template_key -import ayon_core.hosts.unreal.lib as unreal_lib -from ayon_core.hosts.unreal.ue_workers import ( - UEProjectGenerationWorker, - UEPluginInstallWorker -) -from ayon_core.hosts.unreal.ui import SplashScreen - - -class UnrealPrelaunchHook(PreLaunchHook): - """Hook to handle launching Unreal. - - This hook will check if current workfile path has Unreal - project inside. IF not, it initializes it, and finally it pass - path to the project by environment variable to Unreal launcher - shell script. - - """ - app_groups = {"unreal"} - launch_types = {LaunchTypes.local} - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.signature = f"( {self.__class__.__name__} )" - - def _get_work_filename(self): - # Use last workfile if was found - if self.data.get("last_workfile_path"): - last_workfile = Path(self.data.get("last_workfile_path")) - if last_workfile and last_workfile.exists(): - return last_workfile.name - - # Prepare data for fill data and for getting workfile template key - anatomy = self.data["anatomy"] - project_entity = self.data["project_entity"] - - # Use already prepared workdir data - workdir_data = copy.deepcopy(self.data["workdir_data"]) - task_type = workdir_data.get("task", {}).get("type") - - # QUESTION raise exception if version is part of filename template? - workdir_data["version"] = 1 - workdir_data["ext"] = "uproject" - - # Get workfile template key for current context - workfile_template_key = get_workfile_template_key( - project_entity["name"], - task_type, - self.host_name, - ) - # Fill templates - template_obj = anatomy.get_template_item( - "work", workfile_template_key, "file" - ) - - # Return filename - return template_obj.format_strict(workdir_data) - - def exec_plugin_install(self, engine_path: Path, env: dict = None): - # set up the QThread and worker with necessary signals - env = env or os.environ - q_thread = QtCore.QThread() - ue_plugin_worker = UEPluginInstallWorker() - - q_thread.started.connect(ue_plugin_worker.run) - ue_plugin_worker.setup(engine_path, env) - ue_plugin_worker.moveToThread(q_thread) - - splash_screen = SplashScreen( - "Installing plugin", - resources.get_resource("app_icons", "ue4.png") - ) - - # set up the splash screen with necessary triggers - ue_plugin_worker.installing.connect( - splash_screen.update_top_label_text - ) - ue_plugin_worker.progress.connect(splash_screen.update_progress) - ue_plugin_worker.log.connect(splash_screen.append_log) - ue_plugin_worker.finished.connect(splash_screen.quit_and_close) - ue_plugin_worker.failed.connect(splash_screen.fail) - - splash_screen.start_thread(q_thread) - splash_screen.show_ui() - - if not splash_screen.was_proc_successful(): - raise ApplicationLaunchFailed("Couldn't run the application! " - "Plugin failed to install!") - - def exec_ue_project_gen(self, - engine_version: str, - unreal_project_name: str, - engine_path: Path, - project_dir: Path): - self.log.info(( - f"{self.signature} Creating unreal " - f"project [ {unreal_project_name} ]" - )) - - q_thread = QtCore.QThread() - ue_project_worker = UEProjectGenerationWorker() - ue_project_worker.setup( - engine_version, - self.data["project_name"], - unreal_project_name, - engine_path, - project_dir - ) - ue_project_worker.moveToThread(q_thread) - q_thread.started.connect(ue_project_worker.run) - - splash_screen = SplashScreen( - "Initializing UE project", - resources.get_resource("app_icons", "ue4.png") - ) - - ue_project_worker.stage_begin.connect( - splash_screen.update_top_label_text - ) - ue_project_worker.progress.connect(splash_screen.update_progress) - ue_project_worker.log.connect(splash_screen.append_log) - ue_project_worker.finished.connect(splash_screen.quit_and_close) - ue_project_worker.failed.connect(splash_screen.fail) - - splash_screen.start_thread(q_thread) - splash_screen.show_ui() - - if not splash_screen.was_proc_successful(): - raise ApplicationLaunchFailed("Couldn't run the application! " - "Failed to generate the project!") - - def execute(self): - """Hook entry method.""" - workdir = self.launch_context.env["AYON_WORKDIR"] - executable = str(self.launch_context.executable) - engine_version = self.app_name.split("/")[-1].replace("-", ".") - try: - if int(engine_version.split(".")[0]) < 4 and \ - int(engine_version.split(".")[1]) < 26: - raise ApplicationLaunchFailed(( - f"{self.signature} Old unsupported version of UE " - f"detected - {engine_version}")) - except ValueError: - # there can be string in minor version and in that case - # int cast is failing. This probably happens only with - # early access versions and is of no concert for this check - # so let's keep it quiet. - ... - - unreal_project_filename = self._get_work_filename() - unreal_project_name = os.path.splitext(unreal_project_filename)[0] - # Unreal is sensitive about project names longer then 20 chars - if len(unreal_project_name) > 20: - raise ApplicationLaunchFailed( - f"Project name exceeds 20 characters ({unreal_project_name})!" - ) - - # Unreal doesn't accept non alphabet characters at the start - # of the project name. This is because project name is then used - # in various places inside c++ code and there variable names cannot - # start with non-alpha. We append 'P' before project name to solve it. - # 😱 - if not unreal_project_name[:1].isalpha(): - self.log.warning(( - "Project name doesn't start with alphabet " - f"character ({unreal_project_name}). Appending 'P'" - )) - unreal_project_name = f"P{unreal_project_name}" - unreal_project_filename = f'{unreal_project_name}.uproject' - - project_path = Path(os.path.join(workdir, unreal_project_name)) - - self.log.info(( - f"{self.signature} requested UE version: " - f"[ {engine_version} ]" - )) - - project_path.mkdir(parents=True, exist_ok=True) - - # engine_path points to the specific Unreal Engine root - # so, we are going up from the executable itself 3 levels. - engine_path: Path = Path(executable).parents[3] - - # Check if new env variable exists, and if it does, if the path - # actually contains the plugin. If not, install it. - - built_plugin_path = self.launch_context.env.get( - "AYON_BUILT_UNREAL_PLUGIN", None) - - if unreal_lib.check_built_plugin_existance(built_plugin_path): - self.log.info(( - f"{self.signature} using existing built Ayon plugin from " - f"{built_plugin_path}" - )) - unreal_lib.copy_built_plugin(engine_path, Path(built_plugin_path)) - else: - # Set "AYON_UNREAL_PLUGIN" to current process environment for - # execution of `create_unreal_project` - env_key = "AYON_UNREAL_PLUGIN" - if self.launch_context.env.get(env_key): - self.log.info(( - f"{self.signature} using Ayon plugin from " - f"{self.launch_context.env.get(env_key)}" - )) - if self.launch_context.env.get(env_key): - os.environ[env_key] = self.launch_context.env[env_key] - - if not unreal_lib.check_plugin_existence(engine_path): - self.exec_plugin_install(engine_path) - - project_file = project_path / unreal_project_filename - - if not project_file.is_file(): - with tempfile.TemporaryDirectory() as temp_dir: - self.exec_ue_project_gen(engine_version, - unreal_project_name, - engine_path, - Path(temp_dir)) - try: - self.log.info(( - f"Moving from {temp_dir} to " - f"{project_path.as_posix()}" - )) - shutil.copytree( - temp_dir, project_path, dirs_exist_ok=True) - - except shutil.Error as e: - raise ApplicationLaunchFailed(( - f"{self.signature} Cannot copy directory {temp_dir} " - f"to {project_path.as_posix()} - {e}" - )) from e - - self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version - # Append project file to launch arguments - self.launch_context.launch_args.append( - f"\"{project_file.as_posix()}\"") diff --git a/client/ayon_core/hosts/unreal/integration b/client/ayon_core/hosts/unreal/integration deleted file mode 160000 index 04b35dbf5f..0000000000 --- a/client/ayon_core/hosts/unreal/integration +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 04b35dbf5fc42d905281fc30d3a22b139c1855e5 diff --git a/client/ayon_core/hosts/unreal/lib.py b/client/ayon_core/hosts/unreal/lib.py deleted file mode 100644 index 185853a0aa..0000000000 --- a/client/ayon_core/hosts/unreal/lib.py +++ /dev/null @@ -1,551 +0,0 @@ -# -*- coding: utf-8 -*- -"""Unreal launching and project tools.""" - -import json -import os -import platform -import re -import subprocess -from collections import OrderedDict -from distutils import dir_util -from pathlib import Path -from typing import List - -from ayon_core.settings import get_project_settings - - -def get_engine_versions(env=None): - """Detect Unreal Engine versions. - - This will try to detect location and versions of installed Unreal Engine. - Location can be overridden by `UNREAL_ENGINE_LOCATION` environment - variable. - - .. deprecated:: 3.15.4 - - Args: - env (dict, optional): Environment to use. - - Returns: - OrderedDict: dictionary with version as a key and dir as value. - so the highest version is first. - - Example: - >>> get_engine_versions() - { - "4.23": "C:/Epic Games/UE_4.23", - "4.24": "C:/Epic Games/UE_4.24" - } - - """ - env = env or os.environ - engine_locations = {} - try: - root, dirs, _ = next(os.walk(env["UNREAL_ENGINE_LOCATION"])) - - for directory in dirs: - if directory.startswith("UE"): - try: - ver = re.split(r"[-_]", directory)[1] - except IndexError: - continue - engine_locations[ver] = os.path.join(root, directory) - except KeyError: - # environment variable not set - pass - except OSError: - # specified directory doesn't exist - pass - except StopIteration: - # specified directory doesn't exist - pass - - # if we've got something, terminate auto-detection process - if engine_locations: - return OrderedDict(sorted(engine_locations.items())) - - # else kick in platform specific detection - if platform.system().lower() == "windows": - return OrderedDict(sorted(_win_get_engine_versions().items())) - if platform.system().lower() == "linux": - # on linux, there is no installation and getting Unreal Engine involves - # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. - pass - if platform.system().lower() == "darwin": - return OrderedDict(sorted(_darwin_get_engine_version().items())) - - return OrderedDict() - - -def get_editor_exe_path(engine_path: Path, engine_version: str) -> Path: - """Get UE Editor executable path.""" - ue_path = engine_path / "Engine/Binaries" - - ue_name = "UnrealEditor" - - # handle older versions of Unreal Engine - if engine_version.split(".")[0] == "4": - ue_name = "UE4Editor" - - if platform.system().lower() == "windows": - ue_path /= f"Win64/{ue_name}.exe" - - elif platform.system().lower() == "linux": - ue_path /= f"Linux/{ue_name}" - - elif platform.system().lower() == "darwin": - ue_path /= f"Mac/{ue_name}" - - return ue_path - - -def _win_get_engine_versions(): - """Get Unreal Engine versions on Windows. - - If engines are installed via Epic Games Launcher then there is: - `%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat` - This file is JSON file listing installed stuff, Unreal engines - are marked with `"AppName" = "UE_X.XX"`` like `UE_4.24` - - .. deprecated:: 3.15.4 - - Returns: - dict: version as a key and path as a value. - - """ - install_json_path = os.path.join( - os.getenv("PROGRAMDATA"), - "Epic", - "UnrealEngineLauncher", - "LauncherInstalled.dat", - ) - - return _parse_launcher_locations(install_json_path) - - -def _darwin_get_engine_version() -> dict: - """Get Unreal Engine versions on MacOS. - - It works the same as on Windows, just JSON file location is different. - - .. deprecated:: 3.15.4 - - Returns: - dict: version as a key and path as a value. - - See Also: - :func:`_win_get_engine_versions`. - - """ - install_json_path = os.path.join( - os.getenv("HOME"), - "Library", - "Application Support", - "Epic", - "UnrealEngineLauncher", - "LauncherInstalled.dat", - ) - - return _parse_launcher_locations(install_json_path) - - -def _parse_launcher_locations(install_json_path: str) -> dict: - """This will parse locations from json file. - - .. deprecated:: 3.15.4 - - Args: - install_json_path (str): Path to `LauncherInstalled.dat`. - - Returns: - dict: with unreal engine versions as keys and - paths to those engine installations as value. - - """ - engine_locations = {} - if os.path.isfile(install_json_path): - with open(install_json_path, "r") as ilf: - try: - install_data = json.load(ilf) - except json.JSONDecodeError as e: - raise Exception( - "Invalid `LauncherInstalled.dat file. `" - "Cannot determine Unreal Engine location." - ) from e - - for installation in install_data.get("InstallationList", []): - if installation.get("AppName").startswith("UE_"): - ver = installation.get("AppName").split("_")[1] - engine_locations[ver] = installation.get("InstallLocation") - - return engine_locations - - -def create_unreal_project(project_name: str, - unreal_project_name: str, - ue_version: str, - pr_dir: Path, - engine_path: Path, - dev_mode: bool = False, - env: dict = None) -> None: - """This will create `.uproject` file at specified location. - - As there is no way I know to create a project via command line, this is - easiest option. Unreal project file is basically a JSON file. If we find - the `AYON_UNREAL_PLUGIN` environment variable we assume this is the - location of the Integration Plugin and we copy its content to the project - folder and enable this plugin. - - Args: - project_name (str): Name of the project in AYON. - unreal_project_name (str): Name of the project in Unreal. - ue_version (str): Unreal engine version (like 4.23). - pr_dir (Path): Path to directory where project will be created. - engine_path (Path): Path to Unreal Engine installation. - dev_mode (bool, optional): Flag to trigger C++ style Unreal project - needing Visual Studio and other tools to compile plugins from - sources. This will trigger automatically if `Binaries` - directory is not found in plugin folders as this indicates - this is only source distribution of the plugin. Dev mode - is also set in Settings. - env (dict, optional): Environment to use. If not set, `os.environ`. - - Throws: - NotImplementedError: For unsupported platforms. - - Returns: - None - - Deprecated: - since 3.16.0 - - """ - - preset = get_project_settings(project_name)["unreal"]["project_setup"] - # get unreal engine identifier - # ------------------------------------------------------------------------- - # FIXME (antirotor): As of 4.26 this is problem with UE4 built from - # sources. In that case Engine ID is calculated per machine/user and not - # from Engine files as this code then reads. This then prevents UE4 - # to directly open project as it will complain about project being - # created in different UE4 version. When user convert such project - # to his UE4 version, Engine ID is replaced in uproject file. If some - # other user tries to open it, it will present him with similar error. - - # engine_path should be the location of UE_X.X folder - - ue_editor_exe: Path = get_editor_exe_path(engine_path, ue_version) - cmdlet_project: Path = get_path_to_cmdlet_project(ue_version) - - project_file = pr_dir / f"{unreal_project_name}.uproject" - - print("--- Generating a new project ...") - commandlet_cmd = [ - ue_editor_exe.as_posix(), - cmdlet_project.as_posix(), - "-run=AyonGenerateProject", - project_file.resolve().as_posix() - ] - - if dev_mode or preset["dev_mode"]: - commandlet_cmd.append('-GenerateCode') - - gen_process = subprocess.Popen(commandlet_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - for line in gen_process.stdout: - print(line.decode(), end='') - gen_process.stdout.close() - return_code = gen_process.wait() - - if return_code and return_code != 0: - raise RuntimeError( - (f"Failed to generate '{unreal_project_name}' project! " - f"Exited with return code {return_code}")) - - print("--- Project has been generated successfully.") - - with open(project_file.as_posix(), mode="r+") as pf: - pf_json = json.load(pf) - pf_json["EngineAssociation"] = get_build_id(engine_path, ue_version) - pf.seek(0) - json.dump(pf_json, pf, indent=4) - pf.truncate() - print("--- Engine ID has been written into the project file") - - if dev_mode or preset["dev_mode"]: - u_build_tool = get_path_to_ubt(engine_path, ue_version) - - arch = "Win64" - if platform.system().lower() == "windows": - arch = "Win64" - elif platform.system().lower() == "linux": - arch = "Linux" - elif platform.system().lower() == "darwin": - # we need to test this out - arch = "Mac" - - command1 = [ - u_build_tool.as_posix(), - "-projectfiles", - f"-project={project_file}", - "-progress" - ] - - subprocess.run(command1) - - command2 = [ - u_build_tool.as_posix(), - f"-ModuleWithSuffix={unreal_project_name},3555", - arch, - "Development", - "-TargetType=Editor", - f"-Project={project_file}", - project_file, - "-IgnoreJunk" - ] - - subprocess.run(command2) - - # ensure we have PySide2 installed in engine - python_path = None - if platform.system().lower() == "windows": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Win64/python.exe") - - if platform.system().lower() == "linux": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Linux/bin/python3") - - if platform.system().lower() == "darwin": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Mac/bin/python3") - - if not python_path: - raise NotImplementedError("Unsupported platform") - if not python_path.exists(): - raise RuntimeError(f"Unreal Python not found at {python_path}") - subprocess.check_call( - [python_path.as_posix(), "-m", "pip", "install", "pyside2"]) - - -def get_path_to_uat(engine_path: Path) -> Path: - if platform.system().lower() == "windows": - return engine_path / "Engine/Build/BatchFiles/RunUAT.bat" - - if platform.system().lower() in ["linux", "darwin"]: - return engine_path / "Engine/Build/BatchFiles/RunUAT.sh" - - -def get_compatible_integration( - ue_version: str, integration_root: Path) -> List[Path]: - """Get path to compatible version of integration plugin. - - This will try to get the closest compatible versions to the one - specified in sorted list. - - Args: - ue_version (str): version of the current Unreal Engine. - integration_root (Path): path to built-in integration plugins. - - Returns: - list of Path: Sorted list of paths closest to the specified - version. - - """ - major, minor = ue_version.split(".") - integration_paths = [p for p in integration_root.iterdir() - if p.is_dir()] - - compatible_versions = [] - for i in integration_paths: - # parse version from path - try: - i_major, i_minor = re.search( - r"(?P\d+).(?P\d+)$", i.name).groups() - except AttributeError: - # in case there is no match, just skip to next - continue - - # consider versions with different major so different that they - # are incompatible - if int(major) != int(i_major): - continue - - compatible_versions.append(i) - - sorted(set(compatible_versions)) - return compatible_versions - - -def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project = Path( - os.path.dirname(os.path.abspath(__file__))) - - # For now, only tested on Windows (For Linux and Mac - # it has to be implemented) - cmd_project /= f"integration/UE_{ue_version}" - - # if the integration doesn't exist for current engine version - # try to find the closest to it. - if cmd_project.exists(): - return cmd_project / "CommandletProject/CommandletProject.uproject" - - if compatible_versions := get_compatible_integration( - ue_version, cmd_project.parent - ): - return compatible_versions[-1] / "CommandletProject/CommandletProject.uproject" # noqa: E501 - else: - raise RuntimeError( - ("There are no compatible versions of Unreal " - "integration plugin compatible with running version " - f"of Unreal Engine {ue_version}")) - - -def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: - u_build_tool_path = engine_path / "Engine/Binaries/DotNET" - - if ue_version.split(".")[0] == "4": - u_build_tool_path /= "UnrealBuildTool.exe" - elif ue_version.split(".")[0] == "5": - u_build_tool_path /= "UnrealBuildTool/UnrealBuildTool.exe" - - return Path(u_build_tool_path) - - -def get_build_id(engine_path: Path, ue_version: str) -> str: - ue_modules = Path() - if platform.system().lower() == "windows": - ue_modules_path = engine_path / "Engine/Binaries/Win64" - if ue_version.split(".")[0] == "4": - ue_modules_path /= "UE4Editor.modules" - elif ue_version.split(".")[0] == "5": - ue_modules_path /= "UnrealEditor.modules" - ue_modules = Path(ue_modules_path) - - if platform.system().lower() == "linux": - ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", - "Linux", "UE4Editor.modules")) - - if platform.system().lower() == "darwin": - ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", - "Mac", "UE4Editor.modules")) - - if ue_modules.exists(): - print("--- Loading Engine ID from modules file ...") - with open(ue_modules, "r") as mp: - loaded_modules = json.load(mp) - - if loaded_modules.get("BuildId"): - return "{" + loaded_modules.get("BuildId") + "}" - - -def check_built_plugin_existance(plugin_path) -> bool: - if not plugin_path: - return False - - integration_plugin_path = Path(plugin_path) - - if not integration_plugin_path.is_dir(): - raise RuntimeError("Path to the integration plugin is null!") - - if not (integration_plugin_path / "Binaries").is_dir() \ - or not (integration_plugin_path / "Intermediate").is_dir(): - return False - - return True - - -def copy_built_plugin(engine_path: Path, plugin_path: Path) -> None: - ayon_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" - - if not ayon_plugin_path.is_dir(): - ayon_plugin_path.mkdir(parents=True, exist_ok=True) - - engine_plugin_config_path: Path = ayon_plugin_path / "Config" - engine_plugin_config_path.mkdir(exist_ok=True) - - dir_util._path_created = {} - - dir_util.copy_tree(plugin_path.as_posix(), ayon_plugin_path.as_posix()) - - -def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: - env = env or os.environ - integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) - - if not os.path.isdir(integration_plugin_path): - raise RuntimeError("Path to the integration plugin is null!") - - # Create a path to the plugin in the engine - op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" - - if not op_plugin_path.is_dir(): - return False - - if not (op_plugin_path / "Binaries").is_dir() \ - or not (op_plugin_path / "Intermediate").is_dir(): - return False - - return True - - -def try_installing_plugin(engine_path: Path, env: dict = None) -> None: - env = env or os.environ - - integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) - - if not os.path.isdir(integration_plugin_path): - raise RuntimeError("Path to the integration plugin is null!") - - # Create a path to the plugin in the engine - op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" - - if not op_plugin_path.is_dir(): - op_plugin_path.mkdir(parents=True, exist_ok=True) - - engine_plugin_config_path: Path = op_plugin_path / "Config" - engine_plugin_config_path.mkdir(exist_ok=True) - - dir_util._path_created = {} - - if not (op_plugin_path / "Binaries").is_dir() \ - or not (op_plugin_path / "Intermediate").is_dir(): - _build_and_move_plugin(engine_path, op_plugin_path, env) - - -def _build_and_move_plugin(engine_path: Path, - plugin_build_path: Path, - env: dict = None) -> None: - uat_path: Path = get_path_to_uat(engine_path) - - env = env or os.environ - integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) - - if uat_path.is_file(): - temp_dir: Path = integration_plugin_path.parent / "Temp" - temp_dir.mkdir(exist_ok=True) - uplugin_path: Path = integration_plugin_path / "Ayon.uplugin" - - # in order to successfully build the plugin, - # It must be built outside the Engine directory and then moved - build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}', - 'BuildPlugin', - f'-Plugin={uplugin_path.as_posix()}', - f'-Package={temp_dir.as_posix()}'] - subprocess.run(build_plugin_cmd) - - # Copy the contents of the 'Temp' dir into the - # 'Ayon' directory in the engine - dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix()) - - # We need to also copy the config folder. - # The UAT doesn't include the Config folder in the build - plugin_install_config_path: Path = plugin_build_path / "Config" - integration_plugin_config_path = integration_plugin_path / "Config" - - dir_util.copy_tree(integration_plugin_config_path.as_posix(), - plugin_install_config_path.as_posix()) - - dir_util.remove_tree(temp_dir.as_posix()) diff --git a/client/ayon_core/hosts/unreal/plugins/__init__.py b/client/ayon_core/hosts/unreal/plugins/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_camera.py b/client/ayon_core/hosts/unreal/plugins/create/create_camera.py deleted file mode 100644 index 3ffb9dd70b..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_camera.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -import unreal - -from ayon_core.pipeline import CreatorError -from ayon_core.hosts.unreal.api.pipeline import UNREAL_VERSION -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateCamera(UnrealAssetCreator): - """Create Camera.""" - - identifier = "io.ayon.creators.unreal.camera" - label = "Camera" - product_type = "camera" - icon = "fa.camera" - - def create(self, product_name, instance_data, pre_create_data): - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one object.") - - # Add the current level path to the metadata - if UNREAL_VERSION.major == 5: - world = unreal.UnrealEditorSubsystem().get_editor_world() - else: - world = unreal.EditorLevelLibrary.get_editor_world() - - instance_data["level"] = world.get_path_name() - - super(CreateCamera, self).create( - product_name, - instance_data, - pre_create_data) diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_layout.py b/client/ayon_core/hosts/unreal/plugins/create/create_layout.py deleted file mode 100644 index 9bcddfe507..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_layout.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from ayon_core.hosts.unreal.api.plugin import ( - UnrealActorCreator, -) - - -class CreateLayout(UnrealActorCreator): - """Layout output for character rigs.""" - - identifier = "io.ayon.creators.unreal.layout" - label = "Layout" - product_type = "layout" - icon = "cubes" diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_look.py b/client/ayon_core/hosts/unreal/plugins/create/create_look.py deleted file mode 100644 index edc6d45f2f..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_look.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -import unreal - -from ayon_core.pipeline import CreatorError -from ayon_core.hosts.unreal.api.pipeline import ( - create_folder -) -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator -) -from ayon_core.lib import UILabelDef - - -class CreateLook(UnrealAssetCreator): - """Shader connections defining shape look.""" - - identifier = "io.ayon.creators.unreal.look" - label = "Look" - product_type = "look" - icon = "paint-brush" - - def create(self, product_name, instance_data, pre_create_data): - # We need to set this to True for the parent class to work - pre_create_data["use_selection"] = True - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one asset.") - - selected_asset = selection[0] - - look_directory = "/Game/Ayon/Looks" - - # Create the folder - folder_name = create_folder(look_directory, product_name) - path = f"{look_directory}/{folder_name}" - - instance_data["look"] = path - - # Create a new cube static mesh - ar = unreal.AssetRegistryHelpers.get_asset_registry() - cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") - - # Get the mesh of the selected object - original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset() - materials = original_mesh.get_editor_property('static_materials') - - pre_create_data["members"] = [] - - # Add the materials to the cube - for material in materials: - mat_name = material.get_editor_property('material_slot_name') - object_path = f"{path}/{mat_name}.{mat_name}" - unreal_object = unreal.EditorAssetLibrary.duplicate_loaded_asset( - cube.get_asset(), object_path - ) - - # Remove the default material of the cube object - unreal_object.get_editor_property('static_materials').pop() - - unreal_object.add_material( - material.get_editor_property('material_interface')) - - pre_create_data["members"].append(object_path) - - unreal.EditorAssetLibrary.save_asset(object_path) - - super(CreateLook, self).create( - product_name, - instance_data, - pre_create_data) - - def get_pre_create_attr_defs(self): - return [ - UILabelDef("Select the asset from which to create the look.") - ] diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_render.py b/client/ayon_core/hosts/unreal/plugins/create/create_render.py deleted file mode 100644 index 5a96d9809c..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_render.py +++ /dev/null @@ -1,276 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from ayon_core.hosts.unreal.api.pipeline import ( - UNREAL_VERSION, - create_folder, - get_subsequences, -) -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator -) -from ayon_core.lib import ( - UILabelDef, - UISeparatorDef, - BoolDef, - NumberDef -) - - -class CreateRender(UnrealAssetCreator): - """Create instance for sequence for rendering""" - - identifier = "io.ayon.creators.unreal.render" - label = "Render" - product_type = "render" - icon = "eye" - - def create_instance( - self, instance_data, product_name, pre_create_data, - selected_asset_path, master_seq, master_lvl, seq_data - ): - instance_data["members"] = [selected_asset_path] - instance_data["sequence"] = selected_asset_path - instance_data["master_sequence"] = master_seq - instance_data["master_level"] = master_lvl - instance_data["output"] = seq_data.get('output') - instance_data["frameStart"] = seq_data.get('frame_range')[0] - instance_data["frameEnd"] = seq_data.get('frame_range')[1] - - super(CreateRender, self).create( - product_name, - instance_data, - pre_create_data) - - def create_with_new_sequence( - self, product_name, instance_data, pre_create_data - ): - # If the option to create a new level sequence is selected, - # create a new level sequence and a master level. - - root = "/Game/Ayon/Sequences" - - # Create a new folder for the sequence in root - sequence_dir_name = create_folder(root, product_name) - sequence_dir = f"{root}/{sequence_dir_name}" - - unreal.log_warning(f"sequence_dir: {sequence_dir}") - - # Create the level sequence - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - seq = asset_tools.create_asset( - asset_name=product_name, - package_path=sequence_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew()) - - seq.set_playback_start(pre_create_data.get("start_frame")) - seq.set_playback_end(pre_create_data.get("end_frame")) - - pre_create_data["members"] = [seq.get_path_name()] - - unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) - - # Create the master level - if UNREAL_VERSION.major >= 5: - curr_level = unreal.LevelEditorSubsystem().get_current_level() - else: - world = unreal.EditorLevelLibrary.get_editor_world() - levels = unreal.EditorLevelUtils.get_levels(world) - curr_level = levels[0] if len(levels) else None - if not curr_level: - raise RuntimeError("No level loaded.") - curr_level_path = curr_level.get_outer().get_path_name() - - # If the level path does not start with "/Game/", the current - # level is a temporary, unsaved level. - if curr_level_path.startswith("/Game/"): - if UNREAL_VERSION.major >= 5: - unreal.LevelEditorSubsystem().save_current_level() - else: - unreal.EditorLevelLibrary.save_current_level() - - ml_path = f"{sequence_dir}/{product_name}_MasterLevel" - - if UNREAL_VERSION.major >= 5: - unreal.LevelEditorSubsystem().new_level(ml_path) - else: - unreal.EditorLevelLibrary.new_level(ml_path) - - seq_data = { - "sequence": seq, - "output": f"{seq.get_name()}", - "frame_range": ( - seq.get_playback_start(), - seq.get_playback_end())} - - self.create_instance( - instance_data, product_name, pre_create_data, - seq.get_path_name(), seq.get_path_name(), ml_path, seq_data) - - def create_from_existing_sequence( - self, product_name, instance_data, pre_create_data - ): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [ - a.get_path_name() for a in sel_objects - if a.get_class().get_name() == "LevelSequence"] - - if len(selection) == 0: - raise RuntimeError("Please select at least one Level Sequence.") - - seq_data = None - - for sel in selection: - selected_asset = ar.get_asset_by_object_path(sel).get_asset() - selected_asset_path = selected_asset.get_path_name() - - # Check if the selected asset is a level sequence asset. - if selected_asset.get_class().get_name() != "LevelSequence": - unreal.log_warning( - f"Skipping {selected_asset.get_name()}. It isn't a Level " - "Sequence.") - - if pre_create_data.get("use_hierarchy"): - # The asset name is the the third element of the path which - # contains the map. - # To take the asset name, we remove from the path the prefix - # "/Game/OpenPype/" and then we split the path by "/". - sel_path = selected_asset_path - asset_name = sel_path.replace( - "/Game/Ayon/", "").split("/")[0] - - search_path = f"/Game/Ayon/{asset_name}" - else: - search_path = Path(selected_asset_path).parent.as_posix() - - # Get the master sequence and the master level. - # There should be only one sequence and one level in the directory. - try: - ar_filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[search_path], - recursive_paths=False) - sequences = ar.get_assets(ar_filter) - master_seq = sequences[0].get_asset().get_path_name() - master_seq_obj = sequences[0].get_asset() - ar_filter = unreal.ARFilter( - class_names=["World"], - package_paths=[search_path], - recursive_paths=False) - levels = ar.get_assets(ar_filter) - master_lvl = levels[0].get_asset().get_path_name() - except IndexError: - raise RuntimeError( - "Could not find the hierarchy for the selected sequence.") - - # If the selected asset is the master sequence, we get its data - # and then we create the instance for the master sequence. - # Otherwise, we cycle from the master sequence to find the selected - # sequence and we get its data. This data will be used to create - # the instance for the selected sequence. In particular, - # we get the frame range of the selected sequence and its final - # output path. - master_seq_data = { - "sequence": master_seq_obj, - "output": f"{master_seq_obj.get_name()}", - "frame_range": ( - master_seq_obj.get_playback_start(), - master_seq_obj.get_playback_end())} - - if (selected_asset_path == master_seq or - pre_create_data.get("use_hierarchy")): - seq_data = master_seq_data - else: - seq_data_list = [master_seq_data] - - for seq in seq_data_list: - subscenes = get_subsequences(seq.get('sequence')) - - for sub_seq in subscenes: - sub_seq_obj = sub_seq.get_sequence() - curr_data = { - "sequence": sub_seq_obj, - "output": (f"{seq.get('output')}/" - f"{sub_seq_obj.get_name()}"), - "frame_range": ( - sub_seq.get_start_frame(), - sub_seq.get_end_frame() - 1)} - - # If the selected asset is the current sub-sequence, - # we get its data and we break the loop. - # Otherwise, we add the current sub-sequence data to - # the list of sequences to check. - if sub_seq_obj.get_path_name() == selected_asset_path: - seq_data = curr_data - break - - seq_data_list.append(curr_data) - - # If we found the selected asset, we break the loop. - if seq_data is not None: - break - - # If we didn't find the selected asset, we don't create the - # instance. - if not seq_data: - unreal.log_warning( - f"Skipping {selected_asset.get_name()}. It isn't a " - "sub-sequence of the master sequence.") - continue - - self.create_instance( - instance_data, product_name, pre_create_data, - selected_asset_path, master_seq, master_lvl, seq_data) - - def create(self, product_name, instance_data, pre_create_data): - if pre_create_data.get("create_seq"): - self.create_with_new_sequence( - product_name, instance_data, pre_create_data) - else: - self.create_from_existing_sequence( - product_name, instance_data, pre_create_data) - - def get_pre_create_attr_defs(self): - return [ - UILabelDef( - "Select a Level Sequence to render or create a new one." - ), - BoolDef( - "create_seq", - label="Create a new Level Sequence", - default=False - ), - UILabelDef( - "WARNING: If you create a new Level Sequence, the current\n" - "level will be saved and a new Master Level will be created." - ), - NumberDef( - "start_frame", - label="Start Frame", - default=0, - minimum=-999999, - maximum=999999 - ), - NumberDef( - "end_frame", - label="Start Frame", - default=150, - minimum=-999999, - maximum=999999 - ), - UISeparatorDef(), - UILabelDef( - "The following settings are valid only if you are not\n" - "creating a new sequence." - ), - BoolDef( - "use_hierarchy", - label="Use Hierarchy", - default=False - ), - ] diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_staticmeshfbx.py b/client/ayon_core/hosts/unreal/plugins/create/create_staticmeshfbx.py deleted file mode 100644 index 603b852873..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateStaticMeshFBX(UnrealAssetCreator): - """Create Static Meshes as FBX geometry.""" - - identifier = "io.ayon.creators.unreal.staticmeshfbx" - label = "Static Mesh (FBX)" - product_type = "unrealStaticMesh" - icon = "cube" diff --git a/client/ayon_core/hosts/unreal/plugins/create/create_uasset.py b/client/ayon_core/hosts/unreal/plugins/create/create_uasset.py deleted file mode 100644 index 1cd532c63d..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/create/create_uasset.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from ayon_core.pipeline import CreatorError -from ayon_core.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateUAsset(UnrealAssetCreator): - """Create UAsset.""" - - identifier = "io.ayon.creators.unreal.uasset" - label = "UAsset" - product_type = "uasset" - icon = "cube" - - extension = ".uasset" - - def create(self, product_name, instance_data, pre_create_data): - if pre_create_data.get("use_selection"): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one object.") - - obj = selection[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - - if not sys_path: - raise CreatorError( - f"{Path(obj).name} is not on the disk. Likely it needs to" - "be saved first.") - - if Path(sys_path).suffix != self.extension: - raise CreatorError( - f"{Path(sys_path).name} is not a {self.label}.") - - super(CreateUAsset, self).create( - product_name, - instance_data, - pre_create_data) - - -class CreateUMap(CreateUAsset): - """Create Level.""" - - identifier = "io.ayon.creators.unreal.umap" - label = "Level" - product_type = "uasset" - extension = ".umap" - - def create(self, product_name, instance_data, pre_create_data): - instance_data["families"] = ["umap"] - - super(CreateUMap, self).create( - product_name, - instance_data, - pre_create_data) diff --git a/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py b/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py deleted file mode 100644 index 1f63a1697a..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/inventory/delete_unused_assets.py +++ /dev/null @@ -1,66 +0,0 @@ -import unreal - -from ayon_core.hosts.unreal.api.tools_ui import qt_app_context -from ayon_core.hosts.unreal.api.pipeline import delete_asset_if_unused -from ayon_core.pipeline import InventoryAction - - -class DeleteUnusedAssets(InventoryAction): - """Delete all the assets that are not used in any level. - """ - - label = "Delete Unused Assets" - icon = "trash" - color = "red" - order = 1 - - dialog = None - - def _delete_unused_assets(self, containers): - allowed_families = ["model", "rig"] - - for container in containers: - container_dir = container.get("namespace") - if container.get("family") not in allowed_families: - unreal.log_warning( - f"Container {container_dir} is not supported.") - continue - - asset_content = unreal.EditorAssetLibrary.list_assets( - container_dir, recursive=True, include_folder=False - ) - - delete_asset_if_unused(container, asset_content) - - def _show_confirmation_dialog(self, containers): - from qtpy import QtCore - from ayon_core.tools.utils import SimplePopup - from ayon_core.style import load_stylesheet - - dialog = SimplePopup() - dialog.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.WindowStaysOnTopHint - ) - dialog.setFocusPolicy(QtCore.Qt.StrongFocus) - dialog.setWindowTitle("Delete all unused assets") - dialog.set_message( - "You are about to delete all the assets in the project that \n" - "are not used in any level. Are you sure you want to continue?" - ) - dialog.set_button_text("Delete") - - dialog.on_clicked.connect( - lambda: self._delete_unused_assets(containers) - ) - - dialog.show() - dialog.raise_() - dialog.activateWindow() - dialog.setStyleSheet(load_stylesheet()) - - self.dialog = dialog - - def process(self, containers): - with qt_app_context(): - self._show_confirmation_dialog(containers) diff --git a/client/ayon_core/hosts/unreal/plugins/inventory/update_actors.py b/client/ayon_core/hosts/unreal/plugins/inventory/update_actors.py deleted file mode 100644 index 96965d68e6..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/inventory/update_actors.py +++ /dev/null @@ -1,84 +0,0 @@ -import unreal - -from ayon_core.hosts.unreal.api.pipeline import ( - ls, - replace_static_mesh_actors, - replace_skeletal_mesh_actors, - replace_geometry_cache_actors, -) -from ayon_core.pipeline import InventoryAction - - -def update_assets(containers, selected): - allowed_families = ["model", "rig"] - - # Get all the containers in the Unreal Project - all_containers = ls() - - for container in containers: - container_dir = container.get("namespace") - if container.get("family") not in allowed_families: - unreal.log_warning( - f"Container {container_dir} is not supported.") - continue - - # Get all containers with same asset_name but different objectName. - # These are the containers that need to be updated in the level. - sa_containers = [ - i - for i in all_containers - if ( - i.get("asset_name") == container.get("asset_name") and - i.get("objectName") != container.get("objectName") - ) - ] - - asset_content = unreal.EditorAssetLibrary.list_assets( - container_dir, recursive=True, include_folder=False - ) - - # Update all actors in level - for sa_cont in sa_containers: - sa_dir = sa_cont.get("namespace") - old_content = unreal.EditorAssetLibrary.list_assets( - sa_dir, recursive=True, include_folder=False - ) - - if container.get("family") == "rig": - replace_skeletal_mesh_actors( - old_content, asset_content, selected) - replace_static_mesh_actors( - old_content, asset_content, selected) - elif container.get("family") == "model": - if container.get("loader") == "PointCacheAlembicLoader": - replace_geometry_cache_actors( - old_content, asset_content, selected) - else: - replace_static_mesh_actors( - old_content, asset_content, selected) - - unreal.EditorLevelLibrary.save_current_level() - - -class UpdateAllActors(InventoryAction): - """Update all the Actors in the current level to the version of the asset - selected in the scene manager. - """ - - label = "Replace all Actors in level to this version" - icon = "arrow-up" - - def process(self, containers): - update_assets(containers, False) - - -class UpdateSelectedActors(InventoryAction): - """Update only the selected Actors in the current level to the version - of the asset selected in the scene manager. - """ - - label = "Replace selected Actors in level to this version" - icon = "arrow-up" - - def process(self, containers): - update_assets(containers, True) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py b/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py deleted file mode 100644 index a12f4f41b4..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Alembic Animation.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class AnimationAlembicLoader(plugin.Loader): - """Load Unreal SkeletalMesh from Alembic""" - - product_types = {"animation"} - label = "Import Alembic Animation" - representations = {"abc"} - icon = "cube" - color = "orange" - - def get_task(self, filename, asset_dir, asset_name, replace): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - sm_settings = unreal.AbcStaticMeshSettings() - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, -1.0]) - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - options.set_editor_property( - 'import_type', unreal.AlembicImportType.SKELETAL) - - options.static_mesh_settings = sm_settings - options.conversion_settings = conversion_settings - task.options = options - - return task - - def load(self, context, name, namespace, data): - """Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and ayon container - root = unreal_pipeline.AYON_ASSET_DIR - folder_name = context["folder"]["name"] - folder_path = context["folder"]["path"] - product_type = context["product"]["productType"] - suffix = "_CON" - if folder_name: - asset_name = "{}_{}".format(folder_name, name) - else: - asset_name = "{}".format(name) - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - path = self.filepath_from_context(context) - task = self.get_task(path, asset_dir, asset_name, False) - - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - asset_tools.import_asset_tasks([task]) - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type, - } - unreal_pipeline.imprint( - f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_name = container["asset_name"] - repre_entity = context["representation"] - source_path = get_representation_path(repre_entity) - destination_path = container["namespace"] - - task = self.get_task( - source_path, destination_path, folder_name, True - ) - - # do import fbx and replace existing data - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - asset_tools.import_asset_tasks([task]) - - container_path = f"{container['namespace']}/{container['objectName']}" - - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_animation.py b/client/ayon_core/hosts/unreal/plugins/load/load_animation.py deleted file mode 100644 index f6a612ce53..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_animation.py +++ /dev/null @@ -1,337 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load FBX with animations.""" -import os -import json - -import unreal -from unreal import EditorAssetLibrary -from unreal import MovieSceneSkeletalAnimationTrack -from unreal import MovieSceneSkeletalAnimationSection - -from ayon_core.pipeline.context_tools import get_current_folder_entity -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline - - -class AnimationFBXLoader(plugin.Loader): - """Load Unreal SkeletalMesh from FBX.""" - - product_types = {"animation"} - label = "Import FBX Animation" - representations = {"fbx"} - icon = "cube" - color = "orange" - - def _process(self, path, asset_dir, asset_name, instance_name): - automated = False - actor = None - - task = unreal.AssetImportTask() - task.options = unreal.FbxImportUI() - - if instance_name: - automated = True - # Old method to get the actor - # actor_name = 'PersistentLevel.' + instance_name - # actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name) - actors = unreal.EditorLevelLibrary.get_all_level_actors() - for a in actors: - if a.get_class().get_name() != "SkeletalMeshActor": - continue - if a.get_actor_label() == instance_name: - actor = a - break - if not actor: - raise Exception(f"Could not find actor {instance_name}") - skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton - task.options.set_editor_property('skeleton', skeleton) - - if not actor: - return None - - folder_entity = get_current_folder_entity(fields=["attrib.fps"]) - - task.set_editor_property('filename', path) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', False) - task.set_editor_property('automated', automated) - task.set_editor_property('save', False) - - # set import options here - task.options.set_editor_property( - 'automated_import_should_detect_type', False) - task.options.set_editor_property( - 'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH) - task.options.set_editor_property( - 'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION) - task.options.set_editor_property('import_mesh', False) - task.options.set_editor_property('import_animations', True) - task.options.set_editor_property('override_full_name', True) - - task.options.anim_sequence_import_data.set_editor_property( - 'animation_length', - unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME - ) - task.options.anim_sequence_import_data.set_editor_property( - 'import_meshes_in_bone_hierarchy', False) - task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', False) - task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) - task.options.anim_sequence_import_data.set_editor_property( - 'import_custom_attribute', True) - task.options.anim_sequence_import_data.set_editor_property( - 'import_bone_tracks', True) - task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', False) - task.options.anim_sequence_import_data.set_editor_property( - 'convert_scene', True) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - animation = None - - for a in asset_content: - imported_asset_data = EditorAssetLibrary.find_asset_data(a) - imported_asset = unreal.AssetRegistryHelpers.get_asset( - imported_asset_data) - if imported_asset.__class__ == unreal.AnimSequence: - animation = imported_asset - break - - if animation: - animation.set_editor_property('enable_root_motion', True) - actor.skeletal_mesh_component.set_editor_property( - 'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE) - actor.skeletal_mesh_component.animation_data.set_editor_property( - 'anim_to_play', animation) - - return animation - - def load(self, context, name, namespace, options=None): - """ - Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - root = "/Game/Ayon" - folder_path = context["folder"]["path"] - hierarchy = folder_path.lstrip("/").split("/") - folder_name = hierarchy.pop(-1) - product_type = context["product"]["productType"] - - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/Animations/{folder_name}/{name}", suffix="") - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{hierarchy[0]}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_asset().get_path_name() - - hierarchy_dir = root - for h in hierarchy: - hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_dir = f"{hierarchy_dir}/{folder_name}" - - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{hierarchy_dir}/"], - recursive_paths=True) - levels = ar.get_assets(_filter) - level = levels[0].get_asset().get_path_name() - - unreal.EditorLevelLibrary.save_all_dirty_levels() - unreal.EditorLevelLibrary.load_level(level) - - container_name += suffix - - EditorAssetLibrary.make_directory(asset_dir) - - path = self.filepath_from_context(context) - libpath = path.replace(".fbx", ".json") - - with open(libpath, "r") as fp: - data = json.load(fp) - - instance_name = data.get("instance_name") - - animation = self._process(path, asset_dir, asset_name, instance_name) - - asset_content = EditorAssetLibrary.list_assets( - hierarchy_dir, recursive=True, include_folder=False) - - # Get the sequence for the layout, excluding the camera one. - sequences = [a for a in asset_content - if (EditorAssetLibrary.find_asset_data(a).get_class() == - unreal.LevelSequence.static_class() and - "_camera" not in a.split("/")[-1])] - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - for s in sequences: - sequence = ar.get_asset_by_object_path(s).get_asset() - possessables = [ - p for p in sequence.get_possessables() - if p.get_display_name() == instance_name] - - for p in possessables: - tracks = [ - t for t in p.get_tracks() - if (t.get_class() == - MovieSceneSkeletalAnimationTrack.static_class())] - - for t in tracks: - sections = [ - s for s in t.get_sections() - if (s.get_class() == - MovieSceneSkeletalAnimationSection.static_class())] - - for s in sections: - s.params.set_editor_property('animation', animation) - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "folder_path": folder_path, - "product_type": product_type, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - imported_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) - - for a in imported_content: - EditorAssetLibrary.save_asset(a) - - unreal.EditorLevelLibrary.save_current_level() - unreal.EditorLevelLibrary.load_level(master_level) - - def update(self, container, context): - repre_entity = context["representation"] - folder_name = container["asset_name"] - source_path = get_representation_path(repre_entity) - folder_entity = get_current_folder_entity(fields=["attrib.fps"]) - destination_path = container["namespace"] - - task = unreal.AssetImportTask() - task.options = unreal.FbxImportUI() - - task.set_editor_property('filename', source_path) - task.set_editor_property('destination_path', destination_path) - # strip suffix - task.set_editor_property('destination_name', folder_name) - task.set_editor_property('replace_existing', True) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - task.options.set_editor_property( - 'automated_import_should_detect_type', False) - task.options.set_editor_property( - 'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH) - task.options.set_editor_property( - 'mesh_type_to_import', unreal.FBXImportType.FBXIT_ANIMATION) - task.options.set_editor_property('import_mesh', False) - task.options.set_editor_property('import_animations', True) - task.options.set_editor_property('override_full_name', True) - - task.options.anim_sequence_import_data.set_editor_property( - 'animation_length', - unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME - ) - task.options.anim_sequence_import_data.set_editor_property( - 'import_meshes_in_bone_hierarchy', False) - task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', False) - task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) - task.options.anim_sequence_import_data.set_editor_property( - 'import_custom_attribute', True) - task.options.anim_sequence_import_data.set_editor_property( - 'import_bone_tracks', True) - task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', False) - task.options.anim_sequence_import_data.set_editor_property( - 'convert_scene', True) - - skeletal_mesh = EditorAssetLibrary.load_asset( - container.get('namespace') + "/" + container.get('asset_name')) - skeleton = skeletal_mesh.get_editor_property('skeleton') - task.options.set_editor_property('skeleton', skeleton) - - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - }) - - asset_content = EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - EditorAssetLibrary.delete_directory(path) - - asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_camera.py b/client/ayon_core/hosts/unreal/plugins/load/load_camera.py deleted file mode 100644 index 681c83c6a1..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_camera.py +++ /dev/null @@ -1,591 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load camera from FBX.""" -from pathlib import Path - -import ayon_api - -import unreal -from unreal import ( - EditorAssetLibrary, - EditorLevelLibrary, - EditorLevelUtils, - LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, -) -from ayon_core.pipeline import ( - AYON_CONTAINER_ID, - get_current_project_name, - get_representation_path, -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - generate_sequence, - set_sequence_hierarchy, - create_container, - imprint, -) - - -class CameraLoader(plugin.Loader): - """Load Unreal StaticMesh from FBX""" - - product_types = {"camera"} - label = "Load Camera" - representations = {"fbx"} - icon = "cube" - color = "orange" - - def _import_camera( - self, world, sequence, bindings, import_fbx_settings, import_filename - ): - ue_version = unreal.SystemLibrary.get_engine_version().split('.') - ue_major = int(ue_version[0]) - ue_minor = int(ue_version[1]) - - if ue_major == 4 and ue_minor <= 26: - unreal.SequencerTools.import_fbx( - world, - sequence, - bindings, - import_fbx_settings, - import_filename - ) - elif (ue_major == 4 and ue_minor >= 27) or ue_major == 5: - unreal.SequencerTools.import_level_sequence_fbx( - world, - sequence, - bindings, - import_fbx_settings, - import_filename - ) - else: - raise NotImplementedError( - f"Unreal version {ue_major} not supported") - - def load(self, context, name, namespace, data): - """ - Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and Ayon container - folder_entity = context["folder"] - folder_attributes = folder_entity["attrib"] - folder_path = folder_entity["path"] - hierarchy_parts = folder_path.split("/") - # Remove empty string - hierarchy_parts.pop(0) - # Pop folder name - folder_name = hierarchy_parts.pop(-1) - - root = "/Game/Ayon" - hierarchy_dir = root - hierarchy_dir_list = [] - for h in hierarchy_parts: - hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_dir_list.append(hierarchy_dir) - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else name - - tools = unreal.AssetToolsHelpers().get_asset_tools() - - # Create a unique name for the camera directory - unique_number = 1 - if EditorAssetLibrary.does_directory_exist( - f"{hierarchy_dir}/{folder_name}" - ): - asset_content = EditorAssetLibrary.list_assets( - f"{root}/{folder_name}", recursive=False, include_folder=True - ) - - # Get highest number to make a unique name - folders = [a for a in asset_content - if a[-1] == "/" and f"{name}_" in a] - # Get number from folder name. Splits the string by "_" and - # removes the last element (which is a "/"). - f_numbers = [int(f.split("_")[-1][:-1]) for f in folders] - f_numbers.sort() - unique_number = f_numbers[-1] + 1 if f_numbers else 1 - - asset_dir, container_name = tools.create_unique_asset_name( - f"{hierarchy_dir}/{folder_name}/{name}_{unique_number:02d}", suffix="") - - container_name += suffix - - EditorAssetLibrary.make_directory(asset_dir) - - # Create map for the shot, and create hierarchy of map. If the maps - # already exist, we will use them. - h_dir = hierarchy_dir_list[0] - h_asset = hierarchy_dir[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - if not EditorAssetLibrary.does_asset_exist(master_level): - EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - - level = ( - f"{asset_dir}/{folder_name}_map_camera.{folder_name}_map_camera" - ) - if not EditorAssetLibrary.does_asset_exist(level): - EditorLevelLibrary.new_level( - f"{asset_dir}/{folder_name}_map_camera" - ) - - EditorLevelLibrary.load_level(master_level) - EditorLevelUtils.add_level_to_world( - EditorLevelLibrary.get_editor_world(), - level, - unreal.LevelStreamingDynamic - ) - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(level) - - # Get all the sequences in the hierarchy. It will create them, if - # they don't exist. - frame_ranges = [] - sequences = [] - for (h_dir, h) in zip(hierarchy_dir_list, hierarchy_parts): - root_content = EditorAssetLibrary.list_assets( - h_dir, recursive=False, include_folder=False) - - existing_sequences = [ - EditorAssetLibrary.find_asset_data(asset) - for asset in root_content - if EditorAssetLibrary.find_asset_data( - asset).get_class().get_name() == 'LevelSequence' - ] - - if existing_sequences: - for seq in existing_sequences: - sequences.append(seq.get_asset()) - frame_ranges.append(( - seq.get_asset().get_playback_start(), - seq.get_asset().get_playback_end())) - else: - sequence, frame_range = generate_sequence(h, h_dir) - - sequences.append(sequence) - frame_ranges.append(frame_range) - - EditorAssetLibrary.make_directory(asset_dir) - - cam_seq = tools.create_asset( - asset_name=f"{folder_name}_camera", - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - # Add sequences data to hierarchy - for i in range(len(sequences) - 1): - set_sequence_hierarchy( - sequences[i], sequences[i + 1], - frame_ranges[i][1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1], - [level]) - - clip_in = folder_attributes.get("clipIn") - clip_out = folder_attributes.get("clipOut") - - cam_seq.set_display_rate( - unreal.FrameRate(folder_attributes.get("fps"), 1.0)) - cam_seq.set_playback_start(clip_in) - cam_seq.set_playback_end(clip_out + 1) - set_sequence_hierarchy( - sequences[-1], cam_seq, - frame_ranges[-1][1], - clip_in, clip_out, - [level]) - - settings = unreal.MovieSceneUserImportFBXSettings() - settings.set_editor_property('reduce_keys', False) - - if cam_seq: - path = self.filepath_from_context(context) - self._import_camera( - EditorLevelLibrary.get_editor_world(), - cam_seq, - cam_seq.get_bindings(), - settings, - path - ) - - # Set range of all sections - # Changing the range of the section is not enough. We need to change - # the frame of all the keys in the section. - for possessable in cam_seq.get_possessables(): - for tracks in possessable.get_tracks(): - for section in tracks.get_sections(): - section.set_range(clip_in, clip_out + 1) - for channel in section.get_all_channels(): - for key in channel.get_keys(): - old_time = key.get_time().get_editor_property( - 'frame_number') - old_time_value = old_time.get_editor_property( - 'value') - new_time = old_time_value + ( - clip_in - folder_attributes.get('frameStart') - ) - key.set_time(unreal.FrameNumber(value=new_time)) - - # Create Asset Container - create_container( - container=container_name, path=asset_dir) - - product_type = context["product"]["productType"] - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_name, - "family": product_type, - } - imprint(f"{asset_dir}/{container_name}", data) - - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(master_level) - - # Save all assets in the hierarchy - asset_content = EditorAssetLibrary.list_assets( - hierarchy_dir_list[0], recursive=True, include_folder=False - ) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - curr_level_sequence = LevelSequenceLib.get_current_level_sequence() - curr_time = LevelSequenceLib.get_current_time() - is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() - - editor_subsystem = unreal.UnrealEditorSubsystem() - vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() - - asset_dir = container.get('namespace') - - EditorLevelLibrary.save_current_level() - - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[asset_dir], - recursive_paths=False) - sequences = ar.get_assets(_filter) - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[asset_dir], - recursive_paths=True) - maps = ar.get_assets(_filter) - - # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name()) - - level_sequence = sequences[0].get_asset() - - display_rate = level_sequence.get_display_rate() - playback_start = level_sequence.get_playback_start() - playback_end = level_sequence.get_playback_end() - - sequence_name = f"{container.get('asset')}_camera" - - # Get the actors in the level sequence. - objs = unreal.SequencerTools.get_bound_objects( - unreal.EditorLevelLibrary.get_editor_world(), - level_sequence, - level_sequence.get_bindings(), - unreal.SequencerScriptingRange( - has_start_value=True, - has_end_value=True, - inclusive_start=level_sequence.get_playback_start(), - exclusive_end=level_sequence.get_playback_end() - ) - ) - - # Delete actors from the map - for o in objs: - if o.bound_objects[0].get_class().get_name() == "CineCameraActor": - actor_path = o.bound_objects[0].get_path_name().split(":")[-1] - actor = EditorLevelLibrary.get_actor_reference(actor_path) - EditorLevelLibrary.destroy_actor(actor) - - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to find - # the level sequence. - root = "/Game/Ayon" - namespace = container.get('namespace').replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - sequences = ar.get_assets(_filter) - master_sequence = sequences[0].get_asset() - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_asset().get_path_name() - - sequences = [master_sequence] - - parent = None - sub_scene = None - for s in sequences: - tracks = s.get_master_tracks() - subscene_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if subscene_track: - sections = subscene_track.get_sections() - for ss in sections: - if ss.get_sequence().get_name() == sequence_name: - parent = s - sub_scene = ss - break - sequences.append(ss.get_sequence()) - for i, ss in enumerate(sections): - ss.set_row_index(i) - if parent: - break - - assert parent, "Could not find the parent sequence" - - EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) - - settings = unreal.MovieSceneUserImportFBXSettings() - settings.set_editor_property('reduce_keys', False) - - tools = unreal.AssetToolsHelpers().get_asset_tools() - new_sequence = tools.create_asset( - asset_name=sequence_name, - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - new_sequence.set_display_rate(display_rate) - new_sequence.set_playback_start(playback_start) - new_sequence.set_playback_end(playback_end) - - sub_scene.set_sequence(new_sequence) - - repre_entity = context["representation"] - repre_path = get_representation_path(repre_entity) - self._import_camera( - EditorLevelLibrary.get_editor_world(), - new_sequence, - new_sequence.get_bindings(), - settings, - repre_path - ) - - # Set range of all sections - # Changing the range of the section is not enough. We need to change - # the frame of all the keys in the section. - project_name = get_current_project_name() - folder_path = container.get("folder_path") - if folder_path is None: - folder_path = container.get("asset") - folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) - folder_attributes = folder_entity["attrib"] - - clip_in = folder_attributes["clipIn"] - clip_out = folder_attributes["clipOut"] - frame_start = folder_attributes["frameStart"] - for possessable in new_sequence.get_possessables(): - for tracks in possessable.get_tracks(): - for section in tracks.get_sections(): - section.set_range(clip_in, clip_out + 1) - for channel in section.get_all_channels(): - for key in channel.get_keys(): - old_time = key.get_time().get_editor_property( - 'frame_number') - old_time_value = old_time.get_editor_property( - 'value') - new_time = old_time_value + ( - clip_in - frame_start - ) - key.set_time(unreal.FrameNumber(value=new_time)) - - data = { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - } - imprint(f"{asset_dir}/{container.get('container_name')}", data) - - EditorLevelLibrary.save_current_level() - - asset_content = EditorAssetLibrary.list_assets( - f"{root}/{ms_asset}", recursive=True, include_folder=False) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - EditorLevelLibrary.load_level(master_level) - - if curr_level_sequence: - LevelSequenceLib.open_level_sequence(curr_level_sequence) - LevelSequenceLib.set_current_time(curr_time) - LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) - - editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) - - def remove(self, container): - asset_dir = container.get('namespace') - path = Path(asset_dir) - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[asset_dir], - recursive_paths=False) - sequences = ar.get_assets(_filter) - - if not sequences: - raise Exception("Could not find sequence.") - - world = ar.get_asset_by_object_path( - EditorLevelLibrary.get_editor_world().get_path_name()) - - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[asset_dir], - recursive_paths=True) - maps = ar.get_assets(_filter) - - # There should be only one map in the list - if not maps: - raise Exception("Could not find map.") - - map = maps[0] - - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(map.get_asset().get_path_name()) - - # Remove the camera from the level. - actors = EditorLevelLibrary.get_all_level_actors() - - for a in actors: - if a.__class__ == unreal.CineCameraActor: - EditorLevelLibrary.destroy_actor(a) - - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(world.get_asset().get_path_name()) - - # There should be only one sequence in the path. - sequence_name = sequences[0].asset_name - - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to find - # the level sequence. - root = "/Game/Ayon" - namespace = container.get('namespace').replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - sequences = ar.get_assets(_filter) - master_sequence = sequences[0].get_asset() - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_full_name() - - sequences = [master_sequence] - - parent = None - for s in sequences: - tracks = s.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if subscene_track: - sections = subscene_track.get_sections() - for ss in sections: - if ss.get_sequence().get_name() == sequence_name: - parent = s - subscene_track.remove_section(ss) - break - sequences.append(ss.get_sequence()) - # Update subscenes indexes. - for i, ss in enumerate(sections): - ss.set_row_index(i) - - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map_camera") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): - i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() - if parent: - break - - assert parent, "Could not find the parent sequence" - - # Create a temporary level to delete the layout level. - EditorLevelLibrary.save_all_dirty_levels() - EditorAssetLibrary.make_directory(f"{root}/tmp") - tmp_level = f"{root}/tmp/temp_map" - if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): - EditorLevelLibrary.new_level(tmp_level) - else: - EditorLevelLibrary.load_level(tmp_level) - - # Delete the layout directory. - EditorAssetLibrary.delete_directory(asset_dir) - - EditorLevelLibrary.load_level(master_level) - EditorAssetLibrary.delete_directory(f"{root}/tmp") - - # Check if there isn't any more assets in the parent folder, and - # delete it if not. - asset_content = EditorAssetLibrary.list_assets( - path.parent.as_posix(), recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(path.parent.as_posix()) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py deleted file mode 100644 index ae7d41192a..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ /dev/null @@ -1,251 +0,0 @@ -# -*- coding: utf-8 -*- -"""Loader for published alembics.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) - -import unreal # noqa - - -class PointCacheAlembicLoader(plugin.Loader): - """Load Point Cache from Alembic""" - - product_types = {"model", "pointcache"} - label = "Import Alembic Point Cache" - representations = {"abc"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task( - filename, asset_dir, asset_name, replace, - frame_start=None, frame_end=None - ): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - gc_settings = unreal.AbcGeometryCacheSettings() - conversion_settings = unreal.AbcConversionSettings() - sampling_settings = unreal.AbcSamplingSettings() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - options.set_editor_property( - 'import_type', unreal.AlembicImportType.GEOMETRY_CACHE) - - gc_settings.set_editor_property('flatten_tracks', False) - - conversion_settings.set_editor_property('flip_u', False) - conversion_settings.set_editor_property('flip_v', True) - conversion_settings.set_editor_property( - 'scale', unreal.Vector(x=100.0, y=100.0, z=100.0)) - conversion_settings.set_editor_property( - 'rotation', unreal.Vector(x=-90.0, y=0.0, z=180.0)) - - if frame_start is not None: - sampling_settings.set_editor_property('frame_start', frame_start) - if frame_end is not None: - sampling_settings.set_editor_property('frame_end', frame_end) - - options.geometry_cache_settings = gc_settings - options.conversion_settings = conversion_settings - options.sampling_settings = sampling_settings - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name, - frame_start, frame_end - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False, frame_start, frame_end) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - representation, - frame_start, - frame_end, - product_type, - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": representation["id"], - "parent": representation["versionId"], - "frame_start": frame_start, - "frame_end": frame_end, - "product_type": product_type, - "folder_path": folder_path, - # TODO these should be probably removed - "family": product_type, - "asset": folder_path, - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - folder_entity = context["folder"] - folder_path = folder_entity["path"] - folder_name = folder_entity["name"] - folder_attributes = folder_entity["attrib"] - - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - frame_start = folder_attributes.get("frameStart") - frame_end = folder_attributes.get("frameEnd") - - # If frame start and end are the same, we increase the end frame by - # one, otherwise Unreal will not import it - if frame_start == frame_end: - frame_end += 1 - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name, - frame_start, frame_end) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - context["representation"], - frame_start, - frame_end, - context["product"]["productType"] - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - # Create directory for folder and Ayon container - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - version = context["version"]["version"] - repre_entity = context["representation"] - - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - frame_start = int(container.get("frame_start")) - frame_end = int(container.get("frame_end")) - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name, - frame_start, frame_end) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - frame_start, - frame_end, - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_layout.py b/client/ayon_core/hosts/unreal/plugins/load/load_layout.py deleted file mode 100644 index 49d95c6459..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_layout.py +++ /dev/null @@ -1,916 +0,0 @@ -# -*- coding: utf-8 -*- -"""Loader for layouts.""" -import json -import collections -from pathlib import Path - -import unreal -from unreal import ( - EditorAssetLibrary, - EditorLevelLibrary, - EditorLevelUtils, - AssetToolsHelpers, - FBXImportType, - MovieSceneLevelVisibilityTrack, - MovieSceneSubTrack, - LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, -) -import ayon_api - -from ayon_core.pipeline import ( - discover_loader_plugins, - loaders_from_representation, - load_container, - get_representation_path, - AYON_CONTAINER_ID, - get_current_project_name, -) -from ayon_core.pipeline.context_tools import get_current_folder_entity -from ayon_core.settings import get_current_project_settings -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - generate_sequence, - set_sequence_hierarchy, - create_container, - imprint, - ls, -) - - -class LayoutLoader(plugin.Loader): - """Load Layout from a JSON file""" - - product_types = {"layout"} - representations = {"json"} - - label = "Load Layout" - icon = "code-fork" - color = "orange" - ASSET_ROOT = "/Game/Ayon" - - def _get_asset_containers(self, path): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - asset_content = EditorAssetLibrary.list_assets( - path, recursive=True) - - asset_containers = [] - - # Get all the asset containers - for a in asset_content: - obj = ar.get_asset_by_object_path(a) - if obj.get_asset().get_class().get_name() == 'AyonAssetContainer': - asset_containers.append(obj) - - return asset_containers - - @staticmethod - def _get_fbx_loader(loaders, family): - name = "" - if family == 'rig': - name = "SkeletalMeshFBXLoader" - elif family == 'model': - name = "StaticMeshFBXLoader" - elif family == 'camera': - name = "CameraLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - @staticmethod - def _get_abc_loader(loaders, family): - name = "" - if family == 'rig': - name = "SkeletalMeshAlembicLoader" - elif family == 'model': - name = "StaticMeshAlembicLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - def _transform_from_basis(self, transform, basis): - """Transform a transform from a basis to a new basis.""" - # Get the basis matrix - basis_matrix = unreal.Matrix( - basis[0], - basis[1], - basis[2], - basis[3] - ) - transform_matrix = unreal.Matrix( - transform[0], - transform[1], - transform[2], - transform[3] - ) - - new_transform = ( - basis_matrix.get_inverse() * transform_matrix * basis_matrix) - - return new_transform.transform() - - def _process_family( - self, assets, class_name, transform, basis, sequence, inst_name=None - ): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - actors = [] - bindings = [] - - for asset in assets: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() == class_name: - t = self._transform_from_basis(transform, basis) - actor = EditorLevelLibrary.spawn_actor_from_object( - obj, t.translation - ) - actor.set_actor_rotation(t.rotation.rotator(), False) - actor.set_actor_scale3d(t.scale3d) - - if class_name == 'SkeletalMesh': - skm_comp = actor.get_editor_property( - 'skeletal_mesh_component') - skm_comp.set_bounds_scale(10.0) - - actors.append(actor) - - if sequence: - binding = None - for p in sequence.get_possessables(): - if p.get_name() == actor.get_name(): - binding = p - break - - if not binding: - binding = sequence.add_possessable(actor) - - bindings.append(binding) - - return actors, bindings - - def _import_animation( - self, asset_dir, path, instance_name, skeleton, actors_dict, - animation_file, bindings_dict, sequence - ): - anim_file = Path(animation_file) - anim_file_name = anim_file.with_suffix('') - - anim_path = f"{asset_dir}/animations/{anim_file_name}" - - folder_entity = get_current_folder_entity() - # Import animation - task = unreal.AssetImportTask() - task.options = unreal.FbxImportUI() - - task.set_editor_property( - 'filename', str(path.with_suffix(f".{animation_file}"))) - task.set_editor_property('destination_path', anim_path) - task.set_editor_property( - 'destination_name', f"{instance_name}_animation") - task.set_editor_property('replace_existing', False) - task.set_editor_property('automated', True) - task.set_editor_property('save', False) - - # set import options here - task.options.set_editor_property( - 'automated_import_should_detect_type', False) - task.options.set_editor_property( - 'original_import_type', FBXImportType.FBXIT_SKELETAL_MESH) - task.options.set_editor_property( - 'mesh_type_to_import', FBXImportType.FBXIT_ANIMATION) - task.options.set_editor_property('import_mesh', False) - task.options.set_editor_property('import_animations', True) - task.options.set_editor_property('override_full_name', True) - task.options.set_editor_property('skeleton', skeleton) - - task.options.anim_sequence_import_data.set_editor_property( - 'animation_length', - unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME - ) - task.options.anim_sequence_import_data.set_editor_property( - 'import_meshes_in_bone_hierarchy', False) - task.options.anim_sequence_import_data.set_editor_property( - 'use_default_sample_rate', False) - task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) - task.options.anim_sequence_import_data.set_editor_property( - 'import_custom_attribute', True) - task.options.anim_sequence_import_data.set_editor_property( - 'import_bone_tracks', True) - task.options.anim_sequence_import_data.set_editor_property( - 'remove_redundant_keys', False) - task.options.anim_sequence_import_data.set_editor_property( - 'convert_scene', True) - - AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - asset_content = unreal.EditorAssetLibrary.list_assets( - anim_path, recursive=False, include_folder=False - ) - - animation = None - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a) - imported_asset = unreal.AssetRegistryHelpers.get_asset( - imported_asset_data) - if imported_asset.__class__ == unreal.AnimSequence: - animation = imported_asset - break - - if animation: - actor = None - if actors_dict.get(instance_name): - for a in actors_dict.get(instance_name): - if a.get_class().get_name() == 'SkeletalMeshActor': - actor = a - break - - animation.set_editor_property('enable_root_motion', True) - actor.skeletal_mesh_component.set_editor_property( - 'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE) - actor.skeletal_mesh_component.animation_data.set_editor_property( - 'anim_to_play', animation) - - if sequence: - # Add animation to the sequencer - bindings = bindings_dict.get(instance_name) - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - for binding in bindings: - tracks = binding.get_tracks() - track = None - track = tracks[0] if tracks else binding.add_track( - unreal.MovieSceneSkeletalAnimationTrack) - - sections = track.get_sections() - section = None - if not sections: - section = track.add_section() - else: - section = sections[0] - - sec_params = section.get_editor_property('params') - curr_anim = sec_params.get_editor_property('animation') - - if curr_anim: - # Checks if the animation path has a container. - # If it does, it means that the animation is - # already in the sequencer. - anim_path = str(Path( - curr_anim.get_path_name()).parent - ).replace('\\', '/') - - _filter = unreal.ARFilter( - class_names=["AyonAssetContainer"], - package_paths=[anim_path], - recursive_paths=False) - containers = ar.get_assets(_filter) - - if len(containers) > 0: - return - - section.set_range( - sequence.get_playback_start(), - sequence.get_playback_end()) - sec_params = section.get_editor_property('params') - sec_params.set_editor_property('animation', animation) - - def _get_repre_entities_by_version_id(self, data): - version_ids = { - element.get("version") - for element in data - if element.get("representation") - } - version_ids.discard(None) - - output = collections.defaultdict(list) - if not version_ids: - return output - - project_name = get_current_project_name() - repre_entities = ayon_api.get_representations( - project_name, - representation_names={"fbx", "abc"}, - version_ids=version_ids, - fields={"id", "versionId", "name"} - ) - for repre_entity in repre_entities: - version_id = repre_entity["versionId"] - output[version_id].append(repre_entity) - return output - - def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - with open(lib_path, "r") as fp: - data = json.load(fp) - - all_loaders = discover_loader_plugins() - - if not repr_loaded: - repr_loaded = [] - - path = Path(lib_path) - - skeleton_dict = {} - actors_dict = {} - bindings_dict = {} - - loaded_assets = [] - - repre_entities_by_version_id = self._get_repre_entities_by_version_id( - data - ) - for element in data: - repre_id = None - repr_format = None - if element.get('representation'): - version_id = element.get("version") - repre_entities = repre_entities_by_version_id[version_id] - if not repre_entities: - self.log.error( - f"No valid representation found for version" - f" {version_id}") - continue - repre_entity = repre_entities[0] - repre_id = repre_entity["id"] - repr_format = repre_entity["name"] - - # This is to keep compatibility with old versions of the - # json format. - elif element.get('reference_fbx'): - repre_id = element.get('reference_fbx') - repr_format = 'fbx' - elif element.get('reference_abc'): - repre_id = element.get('reference_abc') - repr_format = 'abc' - - # If reference is None, this element is skipped, as it cannot be - # imported in Unreal - if not repre_id: - continue - - instance_name = element.get('instance_name') - - skeleton = None - - if repre_id not in repr_loaded: - repr_loaded.append(repre_id) - - product_type = element.get("product_type") - if product_type is None: - product_type = element.get("family") - loaders = loaders_from_representation( - all_loaders, repre_id) - - loader = None - - if repr_format == 'fbx': - loader = self._get_fbx_loader(loaders, product_type) - elif repr_format == 'abc': - loader = self._get_abc_loader(loaders, product_type) - - if not loader: - self.log.error( - f"No valid loader found for {repre_id}") - continue - - options = { - # "asset_dir": asset_dir - } - - assets = load_container( - loader, - repre_id, - namespace=instance_name, - options=options - ) - - container = None - - for asset in assets: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() == 'AyonAssetContainer': - container = obj - if obj.get_class().get_name() == 'Skeleton': - skeleton = obj - - loaded_assets.append(container.get_path_name()) - - instances = [ - item for item in data - if ((item.get('version') and - item.get('version') == element.get('version')) or - item.get('reference_fbx') == repre_id or - item.get('reference_abc') == repre_id)] - - for instance in instances: - # transform = instance.get('transform') - transform = instance.get('transform_matrix') - basis = instance.get('basis') - inst = instance.get('instance_name') - - actors = [] - - if product_type == 'model': - actors, _ = self._process_family( - assets, 'StaticMesh', transform, basis, - sequence, inst - ) - elif product_type == 'rig': - actors, bindings = self._process_family( - assets, 'SkeletalMesh', transform, basis, - sequence, inst - ) - actors_dict[inst] = actors - bindings_dict[inst] = bindings - - if skeleton: - skeleton_dict[repre_id] = skeleton - else: - skeleton = skeleton_dict.get(repre_id) - - animation_file = element.get('animation') - - if animation_file and skeleton: - self._import_animation( - asset_dir, path, instance_name, skeleton, actors_dict, - animation_file, bindings_dict, sequence) - - return loaded_assets - - @staticmethod - def _remove_family(assets, components, class_name, prop_name): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - objects = [] - for a in assets: - obj = ar.get_asset_by_object_path(a) - if obj.get_asset().get_class().get_name() == class_name: - objects.append(obj) - for obj in objects: - for comp in components: - if comp.get_editor_property(prop_name) == obj.get_asset(): - comp.get_owner().destroy_actor() - - def _remove_actors(self, path): - asset_containers = self._get_asset_containers(path) - - # Get all the static and skeletal meshes components in the level - components = EditorLevelLibrary.get_all_level_actors_components() - static_meshes_comp = [ - c for c in components - if c.get_class().get_name() == 'StaticMeshComponent'] - skel_meshes_comp = [ - c for c in components - if c.get_class().get_name() == 'SkeletalMeshComponent'] - - # For all the asset containers, get the static and skeletal meshes. - # Then, check the components in the level and destroy the matching - # actors. - for asset_container in asset_containers: - package_path = asset_container.get_editor_property('package_path') - family = EditorAssetLibrary.get_metadata_tag( - asset_container.get_asset(), "family") - assets = EditorAssetLibrary.list_assets( - str(package_path), recursive=False) - if family == 'model': - self._remove_family( - assets, static_meshes_comp, 'StaticMesh', 'static_mesh') - elif family == 'rig': - self._remove_family( - assets, skel_meshes_comp, 'SkeletalMesh', 'skeletal_mesh') - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] - - # Create directory for asset and Ayon container - folder_entity = context["folder"] - folder_path = folder_entity["path"] - hierarchy = folder_path.lstrip("/").split("/") - # Remove folder name - folder_name = hierarchy.pop(-1) - root = self.ASSET_ROOT - hierarchy_dir = root - hierarchy_dir_list = [] - for h in hierarchy: - hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_dir_list.append(hierarchy_dir) - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else name - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(hierarchy_dir, folder_name, name), - suffix="" - ) - - container_name += suffix - - EditorAssetLibrary.make_directory(asset_dir) - - master_level = None - shot = None - sequences = [] - - level = f"{asset_dir}/{folder_name}_map.{folder_name}_map" - EditorLevelLibrary.new_level(f"{asset_dir}/{folder_name}_map") - - if create_sequences: - # Create map for the shot, and create hierarchy of map. If the - # maps already exist, we will use them. - if hierarchy: - h_dir = hierarchy_dir_list[0] - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - if not EditorAssetLibrary.does_asset_exist(master_level): - EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - - if master_level: - EditorLevelLibrary.load_level(master_level) - EditorLevelUtils.add_level_to_world( - EditorLevelLibrary.get_editor_world(), - level, - unreal.LevelStreamingDynamic - ) - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(level) - - # Get all the sequences in the hierarchy. It will create them, if - # they don't exist. - frame_ranges = [] - for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): - root_content = EditorAssetLibrary.list_assets( - h_dir, recursive=False, include_folder=False) - - existing_sequences = [ - EditorAssetLibrary.find_asset_data(asset) - for asset in root_content - if EditorAssetLibrary.find_asset_data( - asset).get_class().get_name() == 'LevelSequence' - ] - - if not existing_sequences: - sequence, frame_range = generate_sequence(h, h_dir) - - sequences.append(sequence) - frame_ranges.append(frame_range) - else: - for e in existing_sequences: - sequences.append(e.get_asset()) - frame_ranges.append(( - e.get_asset().get_playback_start(), - e.get_asset().get_playback_end())) - - shot = tools.create_asset( - asset_name=folder_name, - package_path=asset_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - # sequences and frame_ranges have the same length - for i in range(0, len(sequences) - 1): - set_sequence_hierarchy( - sequences[i], sequences[i + 1], - frame_ranges[i][1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1], - [level]) - - project_name = get_current_project_name() - folder_attributes = ( - ayon_api.get_folder_by_path(project_name, folder_path)["attrib"] - ) - shot.set_display_rate( - unreal.FrameRate(folder_attributes.get("fps"), 1.0)) - shot.set_playback_start(0) - shot.set_playback_end( - folder_attributes.get('clipOut') - - folder_attributes.get('clipIn') - + 1 - ) - if sequences: - set_sequence_hierarchy( - sequences[-1], - shot, - frame_ranges[-1][1], - folder_attributes.get('clipIn'), - folder_attributes.get('clipOut'), - [level]) - - EditorLevelLibrary.load_level(level) - - path = self.filepath_from_context(context) - loaded_assets = self._process(path, asset_dir, shot) - - for s in sequences: - EditorAssetLibrary.save_asset(s.get_path_name()) - - EditorLevelLibrary.save_current_level() - - # Create Asset Container - create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": folder_name, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "family": context["product"]["productType"], - "loaded_assets": loaded_assets - } - imprint( - "{}/{}".format(asset_dir, container_name), data) - - save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir - - asset_content = EditorAssetLibrary.list_assets( - save_dir, recursive=True, include_folder=False) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - if master_level: - EditorLevelLibrary.load_level(master_level) - - return asset_content - - def update(self, container, context): - data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - curr_level_sequence = LevelSequenceLib.get_current_level_sequence() - curr_time = LevelSequenceLib.get_current_time() - is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() - - editor_subsystem = unreal.UnrealEditorSubsystem() - vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() - - root = "/Game/Ayon" - - asset_dir = container.get('namespace') - - folder_entity = context["folder"] - repre_entity = context["representation"] - - hierarchy = folder_entity["path"].lstrip("/").split("/") - first_parent_name = hierarchy[0] - - sequence = None - master_level = None - - if create_sequences: - h_dir = f"{root}/{first_parent_name}" - h_asset = first_parent_name - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - - filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[asset_dir], - recursive_paths=False) - sequences = ar.get_assets(filter) - sequence = sequences[0].get_asset() - - prev_level = None - - if not master_level: - curr_level = unreal.LevelEditorSubsystem().get_current_level() - curr_level_path = curr_level.get_outer().get_path_name() - # If the level path does not start with "/Game/", the current - # level is a temporary, unsaved level. - if curr_level_path.startswith("/Game/"): - prev_level = curr_level_path - - # Get layout level - filter = unreal.ARFilter( - class_names=["World"], - package_paths=[asset_dir], - recursive_paths=False) - levels = ar.get_assets(filter) - - layout_level = levels[0].get_asset().get_path_name() - - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(layout_level) - - # Delete all the actors in the level - actors = unreal.EditorLevelLibrary.get_all_level_actors() - for actor in actors: - unreal.EditorLevelLibrary.destroy_actor(actor) - - if create_sequences: - EditorLevelLibrary.save_current_level() - - EditorAssetLibrary.delete_directory(f"{asset_dir}/animations/") - - source_path = get_representation_path(repre_entity) - - loaded_assets = self._process(source_path, asset_dir, sequence) - - data = { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - "loaded_assets": loaded_assets, - } - imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) - - EditorLevelLibrary.save_current_level() - - save_dir = f"{root}/{first_parent_name}" if create_sequences else asset_dir - - asset_content = EditorAssetLibrary.list_assets( - save_dir, recursive=True, include_folder=False) - - for a in asset_content: - EditorAssetLibrary.save_asset(a) - - if master_level: - EditorLevelLibrary.load_level(master_level) - elif prev_level: - EditorLevelLibrary.load_level(prev_level) - - if curr_level_sequence: - LevelSequenceLib.open_level_sequence(curr_level_sequence) - LevelSequenceLib.set_current_time(curr_time) - LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) - - editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) - - def remove(self, container): - """ - Delete the layout. First, check if the assets loaded with the layout - are used by other layouts. If not, delete the assets. - """ - data = get_current_project_settings() - create_sequences = data["unreal"]["level_sequences_for_layouts"] - - root = "/Game/Ayon" - path = Path(container.get("namespace")) - - containers = ls() - layout_containers = [ - c for c in containers - if (c.get('asset_name') != container.get('asset_name') and - c.get('family') == "layout")] - - # Check if the assets have been loaded by other layouts, and deletes - # them if they haven't. - for asset in eval(container.get('loaded_assets')): - layouts = [ - lc for lc in layout_containers - if asset in lc.get('loaded_assets')] - - if not layouts: - EditorAssetLibrary.delete_directory(str(Path(asset).parent)) - - # Delete the parent folder if there aren't any more - # layouts in it. - asset_content = EditorAssetLibrary.list_assets( - str(Path(asset).parent.parent), recursive=False, - include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory( - str(Path(asset).parent.parent)) - - master_sequence = None - master_level = None - sequences = [] - - if create_sequences: - # Remove the Level Sequence from the parent. - # We need to traverse the hierarchy from the master sequence to - # find the level sequence. - namespace = container.get('namespace').replace(f"{root}/", "") - ms_asset = namespace.split('/')[0] - ar = unreal.AssetRegistryHelpers.get_asset_registry() - _filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - sequences = ar.get_assets(_filter) - master_sequence = sequences[0].get_asset() - _filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"{root}/{ms_asset}"], - recursive_paths=False) - levels = ar.get_assets(_filter) - master_level = levels[0].get_asset().get_path_name() - - sequences = [master_sequence] - - parent = None - for s in sequences: - tracks = s.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if subscene_track: - sections = subscene_track.get_sections() - for ss in sections: - if (ss.get_sequence().get_name() == - container.get('asset')): - parent = s - subscene_track.remove_section(ss) - break - sequences.append(ss.get_sequence()) - # Update subscenes indexes. - i = 0 - for ss in sections: - ss.set_row_index(i) - i += 1 - - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): - i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() - if parent: - break - - assert parent, "Could not find the parent sequence" - - # Create a temporary level to delete the layout level. - EditorLevelLibrary.save_all_dirty_levels() - EditorAssetLibrary.make_directory(f"{root}/tmp") - tmp_level = f"{root}/tmp/temp_map" - if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): - EditorLevelLibrary.new_level(tmp_level) - else: - EditorLevelLibrary.load_level(tmp_level) - - # Delete the layout directory. - EditorAssetLibrary.delete_directory(str(path)) - - if create_sequences: - EditorLevelLibrary.load_level(master_level) - EditorAssetLibrary.delete_directory(f"{root}/tmp") - - # Delete the parent folder if there aren't any more layouts in it. - asset_content = EditorAssetLibrary.list_assets( - str(path.parent), recursive=False, include_folder=True - ) - - if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(str(path.parent)) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py b/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py deleted file mode 100644 index f9d438367b..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py +++ /dev/null @@ -1,451 +0,0 @@ -import json -from pathlib import Path - -import unreal -from unreal import EditorLevelLibrary -import ayon_api - -from ayon_core.pipeline import ( - discover_loader_plugins, - loaders_from_representation, - load_container, - get_representation_path, - AYON_CONTAINER_ID, -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as upipeline - - -class ExistingLayoutLoader(plugin.Loader): - """ - Load Layout for an existing scene, and match the existing assets. - """ - - product_types = {"layout"} - representations = {"json"} - - label = "Load Layout on Existing Scene" - icon = "code-fork" - color = "orange" - ASSET_ROOT = "/Game/Ayon" - - delete_unmatched_assets = True - - @classmethod - def apply_settings(cls, project_settings): - super(ExistingLayoutLoader, cls).apply_settings( - project_settings - ) - cls.delete_unmatched_assets = ( - project_settings["unreal"]["delete_unmatched_assets"] - ) - - @staticmethod - def _create_container( - asset_name, - asset_dir, - folder_path, - representation, - version_id, - product_type - ): - container_name = f"{asset_name}_CON" - - if not unreal.EditorAssetLibrary.does_asset_exist( - f"{asset_dir}/{container_name}" - ): - container = upipeline.create_container(container_name, asset_dir) - else: - ar = unreal.AssetRegistryHelpers.get_asset_registry() - obj = ar.get_asset_by_object_path( - f"{asset_dir}/{container_name}.{container_name}") - container = obj.get_asset() - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - # "loader": str(self.__class__.__name__), - "representation": representation, - "parent": version_id, - "product_type": product_type, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type, - } - - upipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) - - return container.get_path_name() - - @staticmethod - def _get_current_level(): - ue_version = unreal.SystemLibrary.get_engine_version().split('.') - ue_major = ue_version[0] - - if ue_major == '4': - return EditorLevelLibrary.get_editor_world() - elif ue_major == '5': - return unreal.LevelEditorSubsystem().get_current_level() - - raise NotImplementedError( - f"Unreal version {ue_major} not supported") - - def _transform_from_basis(self, transform, basis): - """Transform a transform from a basis to a new basis.""" - # Get the basis matrix - basis_matrix = unreal.Matrix( - basis[0], - basis[1], - basis[2], - basis[3] - ) - transform_matrix = unreal.Matrix( - transform[0], - transform[1], - transform[2], - transform[3] - ) - - new_transform = ( - basis_matrix.get_inverse() * transform_matrix * basis_matrix) - - return new_transform.transform() - - def _spawn_actor(self, obj, lasset): - actor = EditorLevelLibrary.spawn_actor_from_object( - obj, unreal.Vector(0.0, 0.0, 0.0) - ) - - actor.set_actor_label(lasset.get('instance_name')) - - transform = lasset.get('transform_matrix') - basis = lasset.get('basis') - - computed_transform = self._transform_from_basis(transform, basis) - - actor.set_actor_transform(computed_transform, False, True) - - @staticmethod - def _get_fbx_loader(loaders, family): - name = "" - if family == 'rig': - name = "SkeletalMeshFBXLoader" - elif family == 'model' or family == 'staticMesh': - name = "StaticMeshFBXLoader" - elif family == 'camera': - name = "CameraLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - @staticmethod - def _get_abc_loader(loaders, family): - name = "" - if family == 'rig': - name = "SkeletalMeshAlembicLoader" - elif family == 'model': - name = "StaticMeshAlembicLoader" - - if name == "": - return None - - for loader in loaders: - if loader.__name__ == name: - return loader - - return None - - def _load_asset(self, repr_data, representation, instance_name, family): - repr_format = repr_data.get('name') - - all_loaders = discover_loader_plugins() - loaders = loaders_from_representation( - all_loaders, representation) - - loader = None - - if repr_format == 'fbx': - loader = self._get_fbx_loader(loaders, family) - elif repr_format == 'abc': - loader = self._get_abc_loader(loaders, family) - - if not loader: - self.log.error(f"No valid loader found for {representation}") - return [] - - # This option is necessary to avoid importing the assets with a - # different conversion compared to the other assets. For ABC files, - # it is in fact impossible to access the conversion settings. So, - # we must assume that the Maya conversion settings have been applied. - options = { - "default_conversion": True - } - - assets = load_container( - loader, - representation, - namespace=instance_name, - options=options - ) - - return assets - - def _get_valid_repre_entities(self, project_name, version_ids): - valid_formats = ['fbx', 'abc'] - - repre_entities = list(ayon_api.get_representations( - project_name, - representation_names=valid_formats, - version_ids=version_ids - )) - repre_entities_by_version_id = {} - for repre_entity in repre_entities: - version_id = repre_entity["versionId"] - repre_entities_by_version_id[version_id] = repre_entity - return repre_entities_by_version_id - - def _process(self, lib_path, project_name): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - actors = EditorLevelLibrary.get_all_level_actors() - - with open(lib_path, "r") as fp: - data = json.load(fp) - - elements = [] - repre_ids = set() - # Get all the representations in the JSON from the database. - for element in data: - repre_id = element.get('representation') - if repre_id: - repre_ids.add(repre_id) - elements.append(element) - - repre_entities = ayon_api.get_representations( - project_name, representation_ids=repre_ids - ) - repre_entities_by_id = { - repre_entity["id"]: repre_entity - for repre_entity in repre_entities - } - layout_data = [] - version_ids = set() - for element in elements: - repre_id = element.get("representation") - repre_entity = repre_entities_by_id.get(repre_id) - if not repre_entity: - raise AssertionError("Representation not found") - if not ( - repre_entity.get("attrib") - or repre_entity["attrib"].get("path") - ): - raise AssertionError("Representation does not have path") - if not repre_entity.get('context'): - raise AssertionError("Representation does not have context") - - layout_data.append((repre_entity, element)) - version_ids.add(repre_entity["versionId"]) - - repre_parents_by_id = ayon_api.get_representation_parents( - project_name, repre_entities_by_id.keys() - ) - - # Prequery valid repre documents for all elements at once - valid_repre_entities_by_version_id = self._get_valid_repre_entities( - project_name, version_ids) - containers = [] - actors_matched = [] - - for (repre_entity, lasset) in layout_data: - # For every actor in the scene, check if it has a representation in - # those we got from the JSON. If so, create a container for it. - # Otherwise, remove it from the scene. - found = False - repre_id = repre_entity["id"] - repre_parents = repre_parents_by_id[repre_id] - folder_path = repre_parents.folder["path"] - folder_name = repre_parents.folder["name"] - product_name = repre_parents.product["name"] - product_type = repre_parents.product["productType"] - - for actor in actors: - if not actor.get_class().get_name() == 'StaticMeshActor': - continue - if actor in actors_matched: - continue - - # Get the original path of the file from which the asset has - # been imported. - smc = actor.get_editor_property('static_mesh_component') - mesh = smc.get_editor_property('static_mesh') - import_data = mesh.get_editor_property('asset_import_data') - filename = import_data.get_first_filename() - path = Path(filename) - - if (not path.name or - path.name not in repre_entity["attrib"]["path"]): - continue - - actor.set_actor_label(lasset.get('instance_name')) - - mesh_path = Path(mesh.get_path_name()).parent.as_posix() - - # Create the container for the asset. - container = self._create_container( - f"{folder_name}_{product_name}", - mesh_path, - folder_path, - repre_entity["id"], - repre_entity["versionId"], - product_type - ) - containers.append(container) - - # Set the transform for the actor. - transform = lasset.get('transform_matrix') - basis = lasset.get('basis') - - computed_transform = self._transform_from_basis( - transform, basis) - actor.set_actor_transform(computed_transform, False, True) - - actors_matched.append(actor) - found = True - break - - # If an actor has not been found for this representation, - # we check if it has been loaded already by checking all the - # loaded containers. If so, we add it to the scene. Otherwise, - # we load it. - if found: - continue - - all_containers = upipeline.ls() - - loaded = False - - for container in all_containers: - repre_id = container.get('representation') - - if not repre_id == repre_entity["id"]: - continue - - asset_dir = container.get('namespace') - - arfilter = unreal.ARFilter( - class_names=["StaticMesh"], - package_paths=[asset_dir], - recursive_paths=False) - assets = ar.get_assets(arfilter) - - for asset in assets: - obj = asset.get_asset() - self._spawn_actor(obj, lasset) - - loaded = True - break - - # If the asset has not been loaded yet, we load it. - if loaded: - continue - - version_id = lasset.get('version') - assets = self._load_asset( - valid_repre_entities_by_version_id.get(version_id), - lasset.get('representation'), - lasset.get('instance_name'), - lasset.get('family') - ) - - for asset in assets: - obj = ar.get_asset_by_object_path(asset).get_asset() - if not obj.get_class().get_name() == 'StaticMesh': - continue - self._spawn_actor(obj, lasset) - - break - - # Check if an actor was not matched to a representation. - # If so, remove it from the scene. - for actor in actors: - if not actor.get_class().get_name() == 'StaticMeshActor': - continue - if actor not in actors_matched: - self.log.warning(f"Actor {actor.get_name()} not matched.") - if self.delete_unmatched_assets: - EditorLevelLibrary.destroy_actor(actor) - - return containers - - def load(self, context, name, namespace, options): - print("Loading Layout and Match Assets") - - folder_name = context["folder"]["name"] - folder_path = context["folder"]["path"] - product_type = context["product"]["productType"] - asset_name = f"{folder_name}_{name}" if folder_name else name - container_name = f"{folder_name}_{name}_CON" - - curr_level = self._get_current_level() - - if not curr_level: - raise AssertionError("Current level not saved") - - project_name = context["project"]["name"] - path = self.filepath_from_context(context) - containers = self._process(path, project_name) - - curr_level_path = Path( - curr_level.get_outer().get_path_name()).parent.as_posix() - - if not unreal.EditorAssetLibrary.does_asset_exist( - f"{curr_level_path}/{container_name}" - ): - upipeline.create_container( - container=container_name, path=curr_level_path) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": curr_level_path, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - "loaded_assets": containers, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type, - } - upipeline.imprint(f"{curr_level_path}/{container_name}", data) - - def update(self, container, context): - asset_dir = container.get('namespace') - - project_name = context["project"]["name"] - repre_entity = context["representation"] - - source_path = get_representation_path(repre_entity) - containers = self._process(source_path, project_name) - - data = { - "representation": repre_entity["id"], - "loaded_assets": containers, - "parent": repre_entity["versionId"], - } - upipeline.imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py deleted file mode 100644 index dfc5d58708..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ /dev/null @@ -1,220 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Skeletal Mesh alembics.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) -import unreal # noqa - - -class SkeletalMeshAlembicLoader(plugin.Loader): - """Load Unreal SkeletalMesh from Alembic""" - - product_types = {"pointcache", "skeletalMesh"} - label = "Import Alembic Skeletal Mesh" - representations = {"abc"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace, default_conversion): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0]) - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - options.set_editor_property( - 'import_type', unreal.AlembicImportType.SKELETAL) - - if not default_conversion: - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0]) - options.conversion_settings = conversion_settings - - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name, - default_conversion=False - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False, default_conversion) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - representation, - product_type - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": representation["id"], - "parent": representation["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type, - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and ayon container - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - default_conversion = False - if options.get("default_conversion"): - default_conversion = options.get("default_conversion") - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize(path, asset_dir, asset_name, - container_name, default_conversion) - - product_type = context["product"]["productType"] - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - context["representation"], - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - version = context["version"]["version"] - repre_entity = context["representation"] - - # Create directory for folder and Ayon container - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize(path, asset_dir, asset_name, - container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type, - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py deleted file mode 100644 index 513404ab98..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ /dev/null @@ -1,222 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Skeletal Meshes form FBX.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) -import unreal # noqa - - -class SkeletalMeshFBXLoader(plugin.Loader): - """Load Unreal SkeletalMesh from FBX.""" - - product_types = {"rig", "skeletalMesh"} - label = "Import FBX Skeletal Mesh" - representations = {"fbx"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace): - task = unreal.AssetImportTask() - options = unreal.FbxImportUI() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - options.set_editor_property( - 'automated_import_should_detect_type', False) - options.set_editor_property('import_as_skeletal', True) - options.set_editor_property('import_animations', False) - options.set_editor_property('import_mesh', True) - options.set_editor_property('import_materials', False) - options.set_editor_property('import_textures', False) - options.set_editor_property('skeleton', None) - options.set_editor_property('create_physics_asset', False) - - options.set_editor_property( - 'mesh_type_to_import', - unreal.FBXImportType.FBXIT_SKELETAL_MESH) - - options.skeletal_mesh_import_data.set_editor_property( - 'import_content_type', - unreal.FBXImportContentType.FBXICT_ALL) - - options.skeletal_mesh_import_data.set_editor_property( - 'normal_import_method', - unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS) - - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - representation, - product_type - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": representation["id"], - "parent": representation["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type, - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - folder_name = context["folder"]["name"] - product_type = context["product"]["productType"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version_entity = context["version"] - # Check if version is hero version and use different name - version = version_entity["version"] - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="" - ) - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name) - - self.imprint( - folder_name, - asset_dir, - container_name, - asset_name, - context["representation"], - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - version = context["version"]["version"] - repre_entity = context["representation"] - - # Create directory for asset and Ayon container - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py deleted file mode 100644 index 0bf6ce9eaa..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ /dev/null @@ -1,223 +0,0 @@ -# -*- coding: utf-8 -*- -"""Loader for Static Mesh alembics.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) -import unreal # noqa - - -class StaticMeshAlembicLoader(plugin.Loader): - """Load Unreal StaticMesh from Alembic""" - - product_types = {"model", "staticMesh"} - label = "Import Alembic Static Mesh" - representations = {"abc"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace, default_conversion): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - sm_settings = unreal.AbcStaticMeshSettings() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - # Unreal 4.24 ignores the settings. It works with Unreal 4.26 - options.set_editor_property( - 'import_type', unreal.AlembicImportType.STATIC_MESH) - - sm_settings.set_editor_property('merge_meshes', True) - - if not default_conversion: - conversion_settings = unreal.AbcConversionSettings( - preset=unreal.AbcConversionPreset.CUSTOM, - flip_u=False, flip_v=False, - rotation=[0.0, 0.0, 0.0], - scale=[1.0, 1.0, 1.0]) - options.conversion_settings = conversion_settings - - options.static_mesh_settings = sm_settings - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name, - default_conversion=False - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False, default_conversion) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - representation, - product_type, - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "folder_path": folder_path, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": representation["id"], - "parent": representation["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - folder_path = context["folder"]["path"] - folder_name = context["folder"]["path"] - - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - default_conversion = False - if options.get("default_conversion"): - default_conversion = options.get("default_conversion") - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize(path, asset_dir, asset_name, - container_name, default_conversion) - - product_type = context["product"]["productType"] - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - context["representation"], - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - repre_entity = context["representation"] - - # Create directory for asset and Ayon container - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize(path, asset_dir, asset_name, - container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py deleted file mode 100644 index b7bb57ac23..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Static meshes form FBX.""" -import os - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api.pipeline import ( - AYON_ASSET_DIR, - create_container, - imprint, -) -import unreal # noqa - - -class StaticMeshFBXLoader(plugin.Loader): - """Load Unreal StaticMesh from FBX.""" - - product_types = {"model", "staticMesh"} - label = "Import FBX Static Mesh" - representations = {"fbx"} - icon = "cube" - color = "orange" - - root = AYON_ASSET_DIR - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace): - task = unreal.AssetImportTask() - options = unreal.FbxImportUI() - import_data = unreal.FbxStaticMeshImportData() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - # set import options here - options.set_editor_property( - 'automated_import_should_detect_type', False) - options.set_editor_property('import_animations', False) - - import_data.set_editor_property('combine_meshes', True) - import_data.set_editor_property('remove_degenerates', False) - - options.static_mesh_import_data = import_data - task.options = options - - return task - - def import_and_containerize( - self, filepath, asset_dir, asset_name, container_name - ): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - task = self.get_task( - filepath, asset_dir, asset_name, False) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - # Create Asset Container - create_container(container=container_name, path=asset_dir) - - def imprint( - self, - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type - ): - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "folder_path": folder_path, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - "product_type": product_type, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type, - } - imprint(f"{asset_dir}/{container_name}", data) - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. - - Returns: - list(str): list of container content - """ - # Create directory for asset and Ayon container - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - version = context["version"]["version"] - # Check if version is hero version and use different name - if version < 0: - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version:03d}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="" - ) - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = self.filepath_from_context(context) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - context["representation"], - context["product"]["productType"] - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - product_name = context["product"]["name"] - product_type = context["product"]["productType"] - version = context["version"]["version"] - repre_entity = context["representation"] - - # Create directory for asset and Ayon container - suffix = "_CON" - asset_name = product_name - if folder_name: - asset_name = f"{folder_name}_{product_name}" - # Check if version is hero version and use different name - if version < 0: - name_version = f"{product_name}_hero" - else: - name_version = f"{product_name}_v{version:03d}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{folder_name}/{name_version}", suffix="") - - container_name += suffix - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_entity) - - self.import_and_containerize( - path, asset_dir, asset_name, container_name) - - self.imprint( - folder_path, - asset_dir, - container_name, - asset_name, - repre_entity, - product_type, - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py b/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py deleted file mode 100644 index 63f23ecc11..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py +++ /dev/null @@ -1,171 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load UAsset.""" -from pathlib import Path -import shutil - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class UAssetLoader(plugin.Loader): - """Load UAsset.""" - - product_types = {"uasset"} - label = "Load UAsset" - representations = {"uasset"} - icon = "cube" - color = "orange" - - extension = "uasset" - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and Ayon container - root = unreal_pipeline.AYON_ASSET_DIR - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{folder_name}/{name}", suffix="" - ) - - unique_number = 1 - while unreal.EditorAssetLibrary.does_directory_exist( - f"{asset_dir}_{unique_number:02}" - ): - unique_number += 1 - - asset_dir = f"{asset_dir}_{unique_number:02}" - container_name = f"{container_name}_{unique_number:02}{suffix}" - - unreal.EditorAssetLibrary.make_directory(asset_dir) - - destination_path = asset_dir.replace( - "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) - - path = self.filepath_from_context(context) - shutil.copy( - path, - f"{destination_path}/{name}_{unique_number:02}.{self.extension}") - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - product_type = context["product"]["productType"] - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "folder_path": folder_path, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - # TODO these should be probably removed - "asset": folder_path, - "family": product_type, - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - asset_dir = container["namespace"] - - product_name = context["product"]["name"] - repre_entity = context["representation"] - - unique_number = container["container_name"].split("_")[-2] - - destination_path = asset_dir.replace( - "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=False, include_folder=True - ) - - for asset in asset_content: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != "AyonAssetContainer": - unreal.EditorAssetLibrary.delete_asset(asset) - - update_filepath = get_representation_path(repre_entity) - - shutil.copy( - update_filepath, - f"{destination_path}/{product_name}_{unique_number}.{self.extension}" - ) - - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - } - ) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = Path(path).parent.as_posix() - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) - - -class UMapLoader(UAssetLoader): - """Load Level.""" - - product_types = {"uasset"} - label = "Load Level" - representations = {"umap"} - - extension = "umap" diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py b/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py deleted file mode 100644 index 708fc83745..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- -"""Loader for Yeti Cache.""" -import os -import json - -from ayon_core.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from ayon_core.hosts.unreal.api import plugin -from ayon_core.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class YetiLoader(plugin.Loader): - """Load Yeti Cache""" - - product_types = {"yeticacheUE"} - label = "Import Yeti" - representations = {"abc"} - icon = "pagelines" - color = "orange" - - @staticmethod - def get_task(filename, asset_dir, asset_name, replace): - task = unreal.AssetImportTask() - options = unreal.AbcImportSettings() - - task.set_editor_property('filename', filename) - task.set_editor_property('destination_path', asset_dir) - task.set_editor_property('destination_name', asset_name) - task.set_editor_property('replace_existing', replace) - task.set_editor_property('automated', True) - task.set_editor_property('save', True) - - task.options = options - - return task - - @staticmethod - def is_groom_module_active(): - """ - Check if Groom plugin is active. - - This is a workaround, because the Unreal python API don't have - any method to check if plugin is active. - """ - prj_file = unreal.Paths.get_project_file_path() - - with open(prj_file, "r") as fp: - data = json.load(fp) - - plugins = data.get("Plugins") - - if not plugins: - return False - - plugin_names = [p.get("Name") for p in plugins] - - return "HairStrands" in plugin_names - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - This is two step process. First, import FBX to temporary path and - then call `containerise()` on it - this moves all content to new - directory and then it will create AssetContainer there and imprint it - with metadata. This will mark this path as container. - - Args: - context (dict): application context - name (str): Product name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - data (dict): Those would be data to be imprinted. This is not used - now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - - """ - # Check if Groom plugin is active - if not self.is_groom_module_active(): - raise RuntimeError("Groom plugin is not activated.") - - # Create directory for asset and Ayon container - root = unreal_pipeline.AYON_ASSET_DIR - folder_path = context["folder"]["path"] - folder_name = context["folder"]["name"] - suffix = "_CON" - asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" - - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{folder_name}/{name}", suffix="") - - unique_number = 1 - while unreal.EditorAssetLibrary.does_directory_exist( - f"{asset_dir}_{unique_number:02}" - ): - unique_number += 1 - - asset_dir = f"{asset_dir}_{unique_number:02}" - container_name = f"{container_name}_{unique_number:02}{suffix}" - - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - unreal.EditorAssetLibrary.make_directory(asset_dir) - - path = self.filepath_from_context(context) - task = self.get_task(path, asset_dir, asset_name, False) - - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - product_type = context["product"]["productType"] - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "namespace": asset_dir, - "container_name": container_name, - "folder_path": folder_path, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["id"], - "parent": context["representation"]["versionId"], - "product_type": product_type, - # TODO these shold be probably removed - "asset": folder_path, - "family": product_type, - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, context): - repre_entity = context["representation"] - name = container["asset_name"] - source_path = get_representation_path(repre_entity) - destination_path = container["namespace"] - - task = self.get_task(source_path, destination_path, name, True) - - # do import fbx and replace existing data - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": repre_entity["id"], - "parent": repre_entity["versionId"], - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - destination_path, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = os.path.dirname(path) - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/collect_current_file.py b/client/ayon_core/hosts/unreal/plugins/publish/collect_current_file.py deleted file mode 100644 index acd4c5c8d2..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/collect_current_file.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect current project path.""" -import unreal # noqa -import pyblish.api - - -class CollectUnrealCurrentFile(pyblish.api.ContextPlugin): - """Inject the current working file into context.""" - - order = pyblish.api.CollectorOrder - 0.5 - label = "Unreal Current File" - hosts = ['unreal'] - - def process(self, context): - """Inject the current working file.""" - current_file = unreal.Paths.get_project_file_path() - context.data['currentFile'] = current_file - - assert current_file != '', "Current file is empty. " \ - "Save the file before continuing." diff --git a/client/ayon_core/hosts/unreal/plugins/publish/collect_instance_members.py b/client/ayon_core/hosts/unreal/plugins/publish/collect_instance_members.py deleted file mode 100644 index de10e7b119..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/collect_instance_members.py +++ /dev/null @@ -1,46 +0,0 @@ -import unreal - -import pyblish.api - - -class CollectInstanceMembers(pyblish.api.InstancePlugin): - """ - Collect members of instance. - - This collector will collect the assets for the families that support to - have them included as External Data, and will add them to the instance - as members. - """ - - order = pyblish.api.CollectorOrder + 0.1 - hosts = ["unreal"] - families = ["camera", "look", "unrealStaticMesh", "uasset"] - label = "Collect Instance Members" - - def process(self, instance): - """Collect members of instance.""" - self.log.info("Collecting instance members") - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - inst_path = instance.data.get('instance_path') - inst_name = inst_path.split('/')[-1] - - pub_instance = ar.get_asset_by_object_path( - f"{inst_path}.{inst_name}").get_asset() - - if not pub_instance: - self.log.error(f"{inst_path}.{inst_name}") - raise RuntimeError(f"Instance {instance} not found.") - - if not pub_instance.get_editor_property("add_external_assets"): - # No external assets in the instance - return - - assets = pub_instance.get_editor_property('asset_data_external') - - members = [asset.get_path_name() for asset in assets] - - self.log.debug(f"Members: {members}") - - instance.data["members"] = members diff --git a/client/ayon_core/hosts/unreal/plugins/publish/collect_remove_marked.py b/client/ayon_core/hosts/unreal/plugins/publish/collect_remove_marked.py deleted file mode 100644 index 69e69f6630..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/collect_remove_marked.py +++ /dev/null @@ -1,24 +0,0 @@ -import pyblish.api - - -class CollectRemoveMarked(pyblish.api.ContextPlugin): - """Remove marked data - - Remove instances that have 'remove' in their instance.data - - """ - - order = pyblish.api.CollectorOrder + 0.499 - label = 'Remove Marked Instances' - - def process(self, context): - - self.log.debug(context) - # make ftrack publishable - instances_to_remove = [] - for instance in context: - if instance.data.get('remove'): - instances_to_remove.append(instance) - - for instance in instances_to_remove: - context.remove(instance) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py b/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py deleted file mode 100644 index ce2a03155b..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/collect_render_instances.py +++ /dev/null @@ -1,116 +0,0 @@ -from pathlib import Path - -import unreal -import pyblish.api - -from ayon_core.pipeline import get_current_project_name -from ayon_core.pipeline import Anatomy -from ayon_core.hosts.unreal.api import pipeline - - -class CollectRenderInstances(pyblish.api.InstancePlugin): - """ This collector will try to find all the rendered frames. - - """ - order = pyblish.api.CollectorOrder - hosts = ["unreal"] - families = ["render"] - label = "Collect Render Instances" - - def process(self, instance): - self.log.debug("Preparing Rendering Instances") - - context = instance.context - - data = instance.data - data['remove'] = True - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sequence = ar.get_asset_by_object_path( - data.get('sequence')).get_asset() - - sequences = [{ - "sequence": sequence, - "output": data.get('output'), - "frame_range": ( - data.get('frameStart'), data.get('frameEnd')) - }] - - for s in sequences: - self.log.debug(f"Processing: {s.get('sequence').get_name()}") - subscenes = pipeline.get_subsequences(s.get('sequence')) - - if subscenes: - for ss in subscenes: - sequences.append({ - "sequence": ss.get_sequence(), - "output": (f"{s.get('output')}/" - f"{ss.get_sequence().get_name()}"), - "frame_range": ( - ss.get_start_frame(), ss.get_end_frame() - 1) - }) - else: - # Avoid creating instances for camera sequences - if "_camera" not in s.get('sequence').get_name(): - seq = s.get('sequence') - seq_name = seq.get_name() - - product_type = "render" - new_product_name = f"{data.get('productName')}_{seq_name}" - new_instance = context.create_instance( - new_product_name - ) - new_instance[:] = seq_name - - new_data = new_instance.data - - new_data["folderPath"] = f"/{s.get('output')}" - new_data["setMembers"] = seq_name - new_data["productName"] = new_product_name - new_data["productType"] = product_type - new_data["family"] = product_type - new_data["families"] = [product_type, "review"] - new_data["parent"] = data.get("parent") - new_data["level"] = data.get("level") - new_data["output"] = s.get('output') - new_data["fps"] = seq.get_display_rate().numerator - new_data["frameStart"] = int(s.get('frame_range')[0]) - new_data["frameEnd"] = int(s.get('frame_range')[1]) - new_data["sequence"] = seq.get_path_name() - new_data["master_sequence"] = data["master_sequence"] - new_data["master_level"] = data["master_level"] - - self.log.debug(f"new instance data: {new_data}") - - try: - project = get_current_project_name() - anatomy = Anatomy(project) - root = anatomy.roots['renders'] - except Exception as e: - raise Exception(( - "Could not find render root " - "in anatomy settings.")) from e - - render_dir = f"{root}/{project}/{s.get('output')}" - render_path = Path(render_dir) - - frames = [] - - for x in render_path.iterdir(): - if x.is_file() and x.suffix == '.png': - frames.append(str(x.name)) - - if "representations" not in new_instance.data: - new_instance.data["representations"] = [] - - repr = { - 'frameStart': instance.data["frameStart"], - 'frameEnd': instance.data["frameEnd"], - 'name': 'png', - 'ext': 'png', - 'files': frames, - 'stagingDir': render_dir, - 'tags': ['review'] - } - new_instance.data["representations"].append(repr) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_camera.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_camera.py deleted file mode 100644 index ebc5452011..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_camera.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -"""Extract camera from Unreal.""" -import os - -import unreal - -from ayon_core.pipeline import publish -from ayon_core.hosts.unreal.api.pipeline import UNREAL_VERSION - - -class ExtractCamera(publish.Extractor): - """Extract a camera.""" - - label = "Extract Camera" - hosts = ["unreal"] - families = ["camera"] - optional = True - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - # Define extract output file path - staging_dir = self.staging_dir(instance) - fbx_filename = "{}.fbx".format(instance.name) - - # Perform extraction - self.log.info("Performing extraction..") - - # Check if the loaded level is the same of the instance - if UNREAL_VERSION.major == 5: - world = unreal.UnrealEditorSubsystem().get_editor_world() - else: - world = unreal.EditorLevelLibrary.get_editor_world() - current_level = world.get_path_name() - assert current_level == instance.data.get("level"), \ - "Wrong level loaded" - - for member in instance.data.get('members'): - data = ar.get_asset_by_object_path(member) - if UNREAL_VERSION.major == 5: - is_level_sequence = ( - data.asset_class_path.asset_name == "LevelSequence") - else: - is_level_sequence = (data.asset_class == "LevelSequence") - - if is_level_sequence: - sequence = data.get_asset() - if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor >= 1: - params = unreal.SequencerExportFBXParams( - world=world, - root_sequence=sequence, - sequence=sequence, - bindings=sequence.get_bindings(), - master_tracks=sequence.get_master_tracks(), - fbx_file_name=os.path.join(staging_dir, fbx_filename) - ) - unreal.SequencerTools.export_level_sequence_fbx(params) - elif UNREAL_VERSION.major == 4 and UNREAL_VERSION.minor == 26: - unreal.SequencerTools.export_fbx( - world, - sequence, - sequence.get_bindings(), - unreal.FbxExportOption(), - os.path.join(staging_dir, fbx_filename) - ) - else: - # Unreal 5.0 or 4.27 - unreal.SequencerTools.export_level_sequence_fbx( - world, - sequence, - sequence.get_bindings(), - unreal.FbxExportOption(), - os.path.join(staging_dir, fbx_filename) - ) - - if not os.path.isfile(os.path.join(staging_dir, fbx_filename)): - raise RuntimeError("Failed to extract camera") - - if "representations" not in instance.data: - instance.data["representations"] = [] - - fbx_representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': fbx_filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(fbx_representation) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py deleted file mode 100644 index 5489057021..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import json -import math - -import unreal -from unreal import EditorLevelLibrary as ell -from unreal import EditorAssetLibrary as eal -import ayon_api - -from ayon_core.pipeline import publish - - -class ExtractLayout(publish.Extractor): - """Extract a layout.""" - - label = "Extract Layout" - hosts = ["unreal"] - families = ["layout"] - optional = True - - def process(self, instance): - # Define extract output file path - staging_dir = self.staging_dir(instance) - - # Perform extraction - self.log.info("Performing extraction..") - - # Check if the loaded level is the same of the instance - current_level = ell.get_editor_world().get_path_name() - assert current_level == instance.data.get("level"), \ - "Wrong level loaded" - - json_data = [] - project_name = instance.context.data["projectName"] - - for member in instance[:]: - actor = ell.get_actor_reference(member) - mesh = None - - # Check type the type of mesh - if actor.get_class().get_name() == 'SkeletalMeshActor': - mesh = actor.skeletal_mesh_component.skeletal_mesh - elif actor.get_class().get_name() == 'StaticMeshActor': - mesh = actor.static_mesh_component.static_mesh - - if mesh: - # Search the reference to the Asset Container for the object - path = unreal.Paths.get_path(mesh.get_path_name()) - filter = unreal.ARFilter( - class_names=["AyonAssetContainer"], package_paths=[path]) - ar = unreal.AssetRegistryHelpers.get_asset_registry() - try: - asset_container = ar.get_assets(filter)[0].get_asset() - except IndexError: - self.log.error("AssetContainer not found.") - return - - parent_id = eal.get_metadata_tag(asset_container, "parent") - family = eal.get_metadata_tag(asset_container, "family") - - self.log.info("Parent: {}".format(parent_id)) - blend = ayon_api.get_representation_by_name( - project_name, "blend", parent_id, fields={"id"} - ) - blend_id = blend["id"] - - json_element = {} - json_element["reference"] = str(blend_id) - json_element["family"] = family - json_element["product_type"] = family - json_element["instance_name"] = actor.get_name() - json_element["asset_name"] = mesh.get_name() - import_data = mesh.get_editor_property("asset_import_data") - json_element["file_path"] = import_data.get_first_filename() - transform = actor.get_actor_transform() - - json_element["transform"] = { - "translation": { - "x": -transform.translation.x, - "y": transform.translation.y, - "z": transform.translation.z - }, - "rotation": { - "x": math.radians(transform.rotation.euler().x), - "y": math.radians(transform.rotation.euler().y), - "z": math.radians(180.0 - transform.rotation.euler().z) - }, - "scale": { - "x": transform.scale3d.x, - "y": transform.scale3d.y, - "z": transform.scale3d.z - } - } - json_data.append(json_element) - - json_filename = "{}.json".format(instance.name) - json_path = os.path.join(staging_dir, json_filename) - - with open(json_path, "w+") as file: - json.dump(json_data, fp=file, indent=2) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - json_representation = { - 'name': 'json', - 'ext': 'json', - 'files': json_filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(json_representation) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_look.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_look.py deleted file mode 100644 index fd1277e302..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_look.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- coding: utf-8 -*- -import json -import os - -import unreal -from unreal import MaterialEditingLibrary as mat_lib - -from ayon_core.pipeline import publish - - -class ExtractLook(publish.Extractor): - """Extract look.""" - - label = "Extract Look" - hosts = ["unreal"] - families = ["look"] - optional = True - - def process(self, instance): - # Define extract output file path - staging_dir = self.staging_dir(instance) - resources_dir = instance.data["resourcesDir"] - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - transfers = [] - - json_data = [] - - for member in instance: - asset = ar.get_asset_by_object_path(member) - obj = asset.get_asset() - - name = asset.get_editor_property('asset_name') - - json_element = {'material': str(name)} - - material_obj = obj.get_editor_property('static_materials')[0] - material = material_obj.material_interface - - base_color = mat_lib.get_material_property_input_node( - material, unreal.MaterialProperty.MP_BASE_COLOR) - - base_color_name = base_color.get_editor_property('parameter_name') - - texture = mat_lib.get_material_default_texture_parameter_value( - material, base_color_name) - - if texture: - # Export Texture - tga_filename = f"{instance.name}_{name}_texture.tga" - - tga_exporter = unreal.TextureExporterTGA() - - tga_export_task = unreal.AssetExportTask() - - tga_export_task.set_editor_property('exporter', tga_exporter) - tga_export_task.set_editor_property('automated', True) - tga_export_task.set_editor_property('object', texture) - tga_export_task.set_editor_property( - 'filename', f"{staging_dir}/{tga_filename}") - tga_export_task.set_editor_property('prompt', False) - tga_export_task.set_editor_property('selected', False) - - unreal.Exporter.run_asset_export_task(tga_export_task) - - json_element['tga_filename'] = tga_filename - - transfers.append(( - f"{staging_dir}/{tga_filename}", - f"{resources_dir}/{tga_filename}")) - - fbx_filename = f"{instance.name}_{name}.fbx" - - fbx_exporter = unreal.StaticMeshExporterFBX() - fbx_exporter.set_editor_property('text', False) - - options = unreal.FbxExportOption() - options.set_editor_property('ascii', False) - options.set_editor_property('collision', False) - - task = unreal.AssetExportTask() - task.set_editor_property('exporter', fbx_exporter) - task.set_editor_property('options', options) - task.set_editor_property('automated', True) - task.set_editor_property('object', object) - task.set_editor_property( - 'filename', f"{staging_dir}/{fbx_filename}") - task.set_editor_property('prompt', False) - task.set_editor_property('selected', False) - - unreal.Exporter.run_asset_export_task(task) - - json_element['fbx_filename'] = fbx_filename - - transfers.append(( - f"{staging_dir}/{fbx_filename}", - f"{resources_dir}/{fbx_filename}")) - - json_data.append(json_element) - - json_filename = f"{instance.name}.json" - json_path = os.path.join(staging_dir, json_filename) - - with open(json_path, "w+") as file: - json.dump(json_data, fp=file, indent=2) - - if "transfers" not in instance.data: - instance.data["transfers"] = [] - if "representations" not in instance.data: - instance.data["representations"] = [] - - json_representation = { - 'name': 'json', - 'ext': 'json', - 'files': json_filename, - "stagingDir": staging_dir, - } - - instance.data["representations"].append(json_representation) - instance.data["transfers"].extend(transfers) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_uasset.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_uasset.py deleted file mode 100644 index fa4fb4c04a..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_uasset.py +++ /dev/null @@ -1,50 +0,0 @@ -from pathlib import Path -import shutil - -import unreal - -from ayon_core.pipeline import publish - - -class ExtractUAsset(publish.Extractor): - """Extract a UAsset.""" - - label = "Extract UAsset" - hosts = ["unreal"] - families = ["uasset", "umap"] - optional = True - - def process(self, instance): - extension = ( - "umap" if "umap" in instance.data.get("families") else "uasset") - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - self.log.debug("Performing extraction..") - staging_dir = self.staging_dir(instance) - - members = instance.data.get("members", []) - - if not members: - raise RuntimeError("No members found in instance.") - - # UAsset publishing supports only one member - obj = members[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - filename = Path(sys_path).name - - shutil.copy(sys_path, staging_dir) - - self.log.info(f"instance.data: {instance.data}") - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": extension, - "ext": extension, - "files": filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/validate_no_dependencies.py b/client/ayon_core/hosts/unreal/plugins/publish/validate_no_dependencies.py deleted file mode 100644 index c760129550..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/validate_no_dependencies.py +++ /dev/null @@ -1,41 +0,0 @@ -import unreal - -import pyblish.api - - -class ValidateNoDependencies(pyblish.api.InstancePlugin): - """Ensure that the uasset has no dependencies - - The uasset is checked for dependencies. If there are any, the instance - cannot be published. - """ - - order = pyblish.api.ValidatorOrder - label = "Check no dependencies" - families = ["uasset"] - hosts = ["unreal"] - optional = True - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - all_dependencies = [] - - for obj in instance[:]: - asset = ar.get_asset_by_object_path(obj) - dependencies = ar.get_dependencies( - asset.package_name, - unreal.AssetRegistryDependencyOptions( - include_soft_package_references=False, - include_hard_package_references=True, - include_searchable_names=False, - include_soft_management_references=False, - include_hard_management_references=False - )) - if dependencies: - for dep in dependencies: - if str(dep).startswith("/Game/"): - all_dependencies.append(str(dep)) - - if all_dependencies: - raise RuntimeError( - f"Dependencies found: {all_dependencies}") diff --git a/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py b/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py deleted file mode 100644 index 85214a2b0d..0000000000 --- a/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ /dev/null @@ -1,83 +0,0 @@ -import clique -import os -import re - -import pyblish.api -from ayon_core.pipeline.publish import PublishValidationError - - -class ValidateSequenceFrames(pyblish.api.InstancePlugin): - """Ensure the sequence of frames is complete - - The files found in the folder are checked against the frameStart and - frameEnd of the instance. If the first or last file is not - corresponding with the first or last frame it is flagged as invalid. - """ - - order = pyblish.api.ValidatorOrder - label = "Validate Sequence Frames" - families = ["render"] - hosts = ["unreal"] - optional = True - - def process(self, instance): - representations = instance.data.get("representations") - folder_attributes = ( - instance.data - .get("folderEntity", {}) - .get("attrib", {}) - ) - for repr in representations: - repr_files = repr["files"] - if isinstance(repr_files, str): - continue - - ext = repr.get("ext") - if not ext: - _, ext = os.path.splitext(repr_files[0]) - elif not ext.startswith("."): - ext = ".{}".format(ext) - pattern = r"\D?(?P(?P0*)\d+){}$".format( - re.escape(ext)) - patterns = [pattern] - - collections, remainder = clique.assemble( - repr["files"], minimum_items=1, patterns=patterns) - - if remainder: - raise PublishValidationError( - "Some files have been found outside a sequence. " - f"Invalid files: {remainder}") - if not collections: - raise PublishValidationError( - "We have been unable to find a sequence in the " - "files. Please ensure the files are named " - "appropriately. " - f"Files: {repr_files}") - if len(collections) > 1: - raise PublishValidationError( - "Multiple collections detected. There should be a single " - "collection per representation. " - f"Collections identified: {collections}") - - collection = collections[0] - frames = list(collection.indexes) - - if instance.data.get("slate"): - # Slate is not part of the frame range - frames = frames[1:] - - current_range = (frames[0], frames[-1]) - required_range = (folder_attributes["clipIn"], - folder_attributes["clipOut"]) - - if current_range != required_range: - raise PublishValidationError( - f"Invalid frame range: {current_range} - " - f"expected: {required_range}") - - missing = collection.holes().indexes - if missing: - raise PublishValidationError( - "Missing frames have been detected. " - f"Missing frames: {missing}") diff --git a/client/ayon_core/hosts/unreal/ue_workers.py b/client/ayon_core/hosts/unreal/ue_workers.py deleted file mode 100644 index 256c0557be..0000000000 --- a/client/ayon_core/hosts/unreal/ue_workers.py +++ /dev/null @@ -1,434 +0,0 @@ -import json -import os -import platform -import re -import subprocess -import tempfile -from distutils import dir_util -from distutils.dir_util import copy_tree -from pathlib import Path -from typing import List, Union - -from qtpy import QtCore - -import ayon_core.hosts.unreal.lib as ue_lib -from ayon_core.settings import get_project_settings - - -def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)): - match = re.search(r"\[[1-9]+/[0-9]+]", line) - if match is not None: - split: list[str] = match.group().split("/") - curr: float = float(split[0][1:]) - total: float = float(split[1][:-1]) - progress_signal.emit(int((curr / total) * 100.0)) - - -def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)): - match = re.search("@progress", line) - if match is not None: - percent_match = re.search(r"\d{1,3}", line) - progress_signal.emit(int(percent_match.group())) - - -def retrieve_exit_code(line: str): - match = re.search(r"ExitCode=\d+", line) - if match is not None: - split: list[str] = match.group().split("=") - return int(split[1]) - - return None - - -class UEWorker(QtCore.QObject): - finished = QtCore.Signal(str) - failed = QtCore.Signal(str, int) - progress = QtCore.Signal(int) - log = QtCore.Signal(str) - - engine_path: Path = None - env = None - - def execute(self): - raise NotImplementedError("Please implement this method!") - - def run(self): - try: - self.execute() - except Exception as e: - import traceback - self.log.emit(str(e)) - self.log.emit(traceback.format_exc()) - self.failed.emit(str(e), 1) - raise e - - -class UEProjectGenerationWorker(UEWorker): - stage_begin = QtCore.Signal(str) - - ue_version: str = None - project_name: str = None - project_dir: Path = None - dev_mode = False - - def setup(self, ue_version: str, - project_name: str, - unreal_project_name, - engine_path: Path, - project_dir: Path, - dev_mode: bool = False, - env: dict = None): - """Set the worker with necessary parameters. - - Args: - ue_version (str): Unreal Engine version. - project_name (str): Name of the project in AYON. - unreal_project_name (str): Name of the project in Unreal. - engine_path (Path): Path to the Unreal Engine. - project_dir (Path): Path to the project directory. - dev_mode (bool, optional): Whether to run the project in dev mode. - Defaults to False. - env (dict, optional): Environment variables. Defaults to None. - - """ - - self.ue_version = ue_version - self.project_dir = project_dir - self.env = env or os.environ - - preset = get_project_settings(project_name)["unreal"]["project_setup"] - - if dev_mode or preset["dev_mode"]: - self.dev_mode = True - - self.project_name = unreal_project_name - self.engine_path = engine_path - - def execute(self): - # engine_path should be the location of UE_X.X folder - - ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path, - self.ue_version) - cmdlet_project = ue_lib.get_path_to_cmdlet_project(self.ue_version) - project_file = self.project_dir / f"{self.project_name}.uproject" - - print("--- Generating a new project ...") - # 1st stage - stage_count = 2 - if self.dev_mode: - stage_count = 4 - - self.stage_begin.emit( - ("Generating a new UE project ... 1 out of " - f"{stage_count}")) - - # Need to copy the commandlet project to a temporary folder where - # users don't need admin rights to write to. - cmdlet_tmp = tempfile.TemporaryDirectory() - cmdlet_filename = cmdlet_project.name - cmdlet_dir = cmdlet_project.parent.as_posix() - cmdlet_tmp_name = Path(cmdlet_tmp.name) - cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename) - copy_tree( - cmdlet_dir, - cmdlet_tmp_name.as_posix()) - - commandlet_cmd = [ - f"{ue_editor_exe.as_posix()}", - f"{cmdlet_tmp_file.as_posix()}", - "-run=AyonGenerateProject", - f"{project_file.resolve().as_posix()}", - ] - - if self.dev_mode: - commandlet_cmd.append("-GenerateCode") - - gen_process = subprocess.Popen(commandlet_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - for line in gen_process.stdout: - decoded_line = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - gen_process.stdout.close() - return_code = gen_process.wait() - - cmdlet_tmp.cleanup() - - if return_code and return_code != 0: - msg = ( - f"Failed to generate {self.project_name} " - f"project! Exited with return code {return_code}" - ) - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - print("--- Project has been generated successfully.") - self.stage_begin.emit( - (f"Writing the Engine ID of the build UE ... 1" - f" out of {stage_count}")) - - if not project_file.is_file(): - msg = ("Failed to write the Engine ID into .uproject file! Can " - "not read!") - self.failed.emit(msg) - raise RuntimeError(msg) - - with open(project_file.as_posix(), mode="r+") as pf: - pf_json = json.load(pf) - pf_json["EngineAssociation"] = ue_lib.get_build_id( - self.engine_path, - self.ue_version - ) - print(pf_json["EngineAssociation"]) - pf.seek(0) - json.dump(pf_json, pf, indent=4) - pf.truncate() - print("--- Engine ID has been written into the project file") - - self.progress.emit(90) - if self.dev_mode: - # 2nd stage - self.stage_begin.emit( - (f"Generating project files ... 2 out of " - f"{stage_count}")) - - self.progress.emit(0) - ubt_path = ue_lib.get_path_to_ubt(self.engine_path, - self.ue_version) - - arch = "Win64" - if platform.system().lower() == "windows": - arch = "Win64" - elif platform.system().lower() == "linux": - arch = "Linux" - elif platform.system().lower() == "darwin": - # we need to test this out - arch = "Mac" - - gen_prj_files_cmd = [ubt_path.as_posix(), - "-projectfiles", - f"-project={project_file}", - "-progress"] - gen_proc = subprocess.Popen(gen_prj_files_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - for line in gen_proc.stdout: - decoded_line: str = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - parse_prj_progress(decoded_line, self.progress) - - gen_proc.stdout.close() - return_code = gen_proc.wait() - - if return_code and return_code != 0: - msg = ("Failed to generate project files! " - f"Exited with return code {return_code}") - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - self.stage_begin.emit( - f"Building the project ... 3 out of {stage_count}") - self.progress.emit(0) - # 3rd stage - build_prj_cmd = [ubt_path.as_posix(), - f"-ModuleWithSuffix={self.project_name},3555", - arch, - "Development", - "-TargetType=Editor", - f"-Project={project_file}", - f"{project_file}", - "-IgnoreJunk"] - - build_prj_proc = subprocess.Popen(build_prj_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - for line in build_prj_proc.stdout: - decoded_line: str = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - parse_comp_progress(decoded_line, self.progress) - - build_prj_proc.stdout.close() - return_code = build_prj_proc.wait() - - if return_code and return_code != 0: - msg = ("Failed to build project! " - f"Exited with return code {return_code}") - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - # ensure we have PySide2/6 installed in engine - - self.progress.emit(0) - self.stage_begin.emit( - (f"Checking Qt bindings installation... {stage_count} " - f" out of {stage_count}")) - python_path = None - if platform.system().lower() == "windows": - python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Win64/python.exe") - - if platform.system().lower() == "linux": - python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Linux/bin/python3") - - if platform.system().lower() == "darwin": - python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Mac/bin/python3") - - if not python_path: - msg = "Unsupported platform" - self.failed.emit(msg, 1) - raise NotImplementedError(msg) - if not python_path.exists(): - msg = f"Unreal Python not found at {python_path}" - self.failed.emit(msg, 1) - raise RuntimeError(msg) - - pyside_version = "PySide2" - ue_version = self.ue_version.split(".") - if int(ue_version[0]) == 5 and int(ue_version[1]) >= 4: - # Use PySide6 6.6.3 because 6.7.0 had a bug - # - 'QPushButton' can't be added to 'QBoxLayout' - pyside_version = "PySide6==6.6.3" - - site_packages_prefix = python_path.parent.as_posix() - - pyside_cmd = [ - python_path.as_posix(), - "-m", "pip", - "install", - "--ignore-installed", - pyside_version, - - ] - - if platform.system().lower() == "windows": - pyside_cmd += ["--target", site_packages_prefix] - - print(f"--- Installing {pyside_version} ...") - print(" ".join(pyside_cmd)) - - pyside_install = subprocess.Popen(pyside_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - - for line in pyside_install.stdout: - decoded_line: str = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - - pyside_install.stdout.close() - return_code = pyside_install.wait() - - if return_code and return_code != 0: - msg = (f"Failed to create the project! {return_code} " - f"The installation of {pyside_version} has failed!: {pyside_install}") - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - self.progress.emit(100) - self.finished.emit("Project successfully built!") - - -class UEPluginInstallWorker(UEWorker): - installing = QtCore.Signal(str) - - def setup(self, engine_path: Path, env: dict = None, ): - self.engine_path = engine_path - self.env = env or os.environ - - def _build_and_move_plugin(self, plugin_build_path: Path): - uat_path: Path = ue_lib.get_path_to_uat(self.engine_path) - src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) - - if not os.path.isdir(src_plugin_dir): - msg = "Path to the integration plugin is null!" - self.failed.emit(msg, 1) - raise RuntimeError(msg) - - if not uat_path.is_file(): - msg = "Building failed! Path to UAT is invalid!" - self.failed.emit(msg, 1) - raise RuntimeError(msg) - - temp_dir: Path = src_plugin_dir.parent / "Temp" - temp_dir.mkdir(exist_ok=True) - uplugin_path: Path = src_plugin_dir / "Ayon.uplugin" - - # in order to successfully build the plugin, - # It must be built outside the Engine directory and then moved - build_plugin_cmd: List[str] = [f"{uat_path.as_posix()}", - "BuildPlugin", - f"-Plugin={uplugin_path.as_posix()}", - f"-Package={temp_dir.as_posix()}"] - - build_proc = subprocess.Popen(build_plugin_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - return_code: Union[None, int] = None - for line in build_proc.stdout: - decoded_line: str = line.decode(errors="replace") - print(decoded_line, end="") - self.log.emit(decoded_line) - if return_code is None: - return_code = retrieve_exit_code(decoded_line) - parse_comp_progress(decoded_line, self.progress) - - build_proc.stdout.close() - build_proc.wait() - - if return_code and return_code != 0: - msg = ("Failed to build plugin" - f" project! Exited with return code {return_code}") - dir_util.remove_tree(temp_dir.as_posix()) - self.failed.emit(msg, return_code) - raise RuntimeError(msg) - - # Copy the contents of the 'Temp' dir into the - # 'Ayon' directory in the engine - dir_util.copy_tree(temp_dir.as_posix(), - plugin_build_path.as_posix()) - - # We need to also copy the config folder. - # The UAT doesn't include the Config folder in the build - plugin_install_config_path: Path = plugin_build_path / "Config" - src_plugin_config_path = src_plugin_dir / "Config" - - dir_util.copy_tree(src_plugin_config_path.as_posix(), - plugin_install_config_path.as_posix()) - - dir_util.remove_tree(temp_dir.as_posix()) - - def execute(self): - src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) - - if not os.path.isdir(src_plugin_dir): - msg = "Path to the integration plugin is null!" - self.failed.emit(msg, 1) - raise RuntimeError(msg) - - # Create a path to the plugin in the engine - op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \ - "/Ayon" - - if not op_plugin_path.is_dir(): - self.installing.emit("Installing and building the plugin ...") - op_plugin_path.mkdir(parents=True, exist_ok=True) - - engine_plugin_config_path = op_plugin_path / "Config" - engine_plugin_config_path.mkdir(exist_ok=True) - - dir_util._path_created = {} - - if not (op_plugin_path / "Binaries").is_dir() \ - or not (op_plugin_path / "Intermediate").is_dir(): - self.installing.emit("Building the plugin ...") - print("--- Building the plugin...") - - self._build_and_move_plugin(op_plugin_path) - - self.finished.emit("Plugin successfully installed") diff --git a/client/ayon_core/hosts/unreal/ui/__init__.py b/client/ayon_core/hosts/unreal/ui/__init__.py deleted file mode 100644 index 606b21ef19..0000000000 --- a/client/ayon_core/hosts/unreal/ui/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .splash_screen import SplashScreen - -__all__ = ( - "SplashScreen", -) diff --git a/client/ayon_core/hosts/unreal/ui/splash_screen.py b/client/ayon_core/hosts/unreal/ui/splash_screen.py deleted file mode 100644 index cf34943515..0000000000 --- a/client/ayon_core/hosts/unreal/ui/splash_screen.py +++ /dev/null @@ -1,262 +0,0 @@ -from qtpy import QtWidgets, QtCore, QtGui -from ayon_core import style, resources - - -class SplashScreen(QtWidgets.QDialog): - """Splash screen for executing a process on another thread. It is able - to inform about the progress of the process and log given information. - """ - - splash_icon = None - top_label = None - show_log_btn: QtWidgets.QLabel = None - progress_bar = None - log_text: QtWidgets.QLabel = None - scroll_area: QtWidgets.QScrollArea = None - close_btn: QtWidgets.QPushButton = None - scroll_bar: QtWidgets.QScrollBar = None - - is_log_visible = False - is_scroll_auto = True - - thread_return_code = None - q_thread: QtCore.QThread = None - - def __init__(self, - window_title: str, - splash_icon=None, - window_icon=None): - """ - Args: - window_title (str): String which sets the window title - splash_icon (str | bytes | None): A resource (pic) which is used - for the splash icon - window_icon (str | bytes | None: A resource (pic) which is used for - the window's icon - """ - super(SplashScreen, self).__init__() - - if splash_icon is None: - splash_icon = resources.get_ayon_icon_filepath() - - if window_icon is None: - window_icon = resources.get_ayon_icon_filepath() - - self.splash_icon = splash_icon - self.setWindowIcon(QtGui.QIcon(window_icon)) - self.setWindowTitle(window_title) - self.init_ui() - - def was_proc_successful(self) -> bool: - return self.thread_return_code == 0 - - def start_thread(self, q_thread: QtCore.QThread): - """Saves the reference to this thread and starts it. - - Args: - q_thread (QtCore.QThread): A QThread containing a given worker - (QtCore.QObject) - - Returns: - None - """ - if not q_thread: - raise RuntimeError("Failed to run a worker thread! " - "The thread is null!") - - self.q_thread = q_thread - self.q_thread.start() - - @QtCore.Slot() - def quit_and_close(self): - """Quits the thread and closes the splash screen. Note that this means - the thread has exited with the return code 0! - - Returns: - None - """ - self.thread_return_code = 0 - self.q_thread.quit() - - if not self.q_thread.wait(5000): - raise RuntimeError("Failed to quit the QThread! " - "The deadline has been reached! The thread " - "has not finished it's execution!.") - self.close() - - - @QtCore.Slot() - def toggle_log(self): - if self.is_log_visible: - self.scroll_area.hide() - width = self.width() - self.adjustSize() - self.resize(width, self.height()) - else: - self.scroll_area.show() - self.scroll_bar.setValue(self.scroll_bar.maximum()) - self.resize(self.width(), 300) - - self.is_log_visible = not self.is_log_visible - - def show_ui(self): - """Shows the splash screen. BEWARE THAT THIS FUNCTION IS BLOCKING - (The execution of code can not proceed further beyond this function - until the splash screen is closed!) - - Returns: - None - """ - self.show() - self.exec_() - - def init_ui(self): - self.resize(450, 100) - self.setMinimumWidth(250) - self.setStyleSheet(style.load_stylesheet()) - - # Top Section - self.top_label = QtWidgets.QLabel(self) - self.top_label.setText("Starting process ...") - self.top_label.setWordWrap(True) - - icon = QtWidgets.QLabel(self) - icon.setPixmap(QtGui.QPixmap(self.splash_icon)) - icon.setFixedHeight(45) - icon.setFixedWidth(45) - icon.setScaledContents(True) - - self.close_btn = QtWidgets.QPushButton(self) - self.close_btn.setText("Quit") - self.close_btn.clicked.connect(self.close) - self.close_btn.setFixedWidth(80) - self.close_btn.hide() - - self.show_log_btn = QtWidgets.QPushButton(self) - self.show_log_btn.setText("Show log") - self.show_log_btn.setFixedWidth(80) - self.show_log_btn.clicked.connect(self.toggle_log) - - button_layout = QtWidgets.QVBoxLayout() - button_layout.addWidget(self.show_log_btn) - button_layout.addWidget(self.close_btn) - - # Progress Bar - self.progress_bar = QtWidgets.QProgressBar() - self.progress_bar.setValue(0) - self.progress_bar.setAlignment(QtCore.Qt.AlignTop) - - # Log Content - self.scroll_area = QtWidgets.QScrollArea(self) - self.scroll_area.hide() - log_widget = QtWidgets.QWidget(self.scroll_area) - self.scroll_area.setWidgetResizable(True) - self.scroll_area.setHorizontalScrollBarPolicy( - QtCore.Qt.ScrollBarAlwaysOn - ) - self.scroll_area.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarAlwaysOn - ) - self.scroll_area.setWidget(log_widget) - - self.scroll_bar = self.scroll_area.verticalScrollBar() - self.scroll_bar.sliderMoved.connect(self.on_scroll) - - self.log_text = QtWidgets.QLabel(self) - self.log_text.setText('') - self.log_text.setAlignment(QtCore.Qt.AlignTop) - - log_layout = QtWidgets.QVBoxLayout(log_widget) - log_layout.addWidget(self.log_text) - - top_layout = QtWidgets.QHBoxLayout() - top_layout.setAlignment(QtCore.Qt.AlignTop) - top_layout.addWidget(icon) - top_layout.addSpacing(10) - top_layout.addWidget(self.top_label) - top_layout.addSpacing(10) - top_layout.addLayout(button_layout) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.addLayout(top_layout) - main_layout.addSpacing(10) - main_layout.addWidget(self.progress_bar) - main_layout.addSpacing(10) - main_layout.addWidget(self.scroll_area) - - self.setWindowFlags( - QtCore.Qt.Window - | QtCore.Qt.CustomizeWindowHint - | QtCore.Qt.WindowTitleHint - | QtCore.Qt.WindowMinimizeButtonHint - ) - - desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(self) - center = desktop_rect.center() - self.move( - center.x() - (self.width() * 0.5), - center.y() - (self.height() * 0.5) - ) - - @QtCore.Slot(int) - def update_progress(self, value: int): - self.progress_bar.setValue(value) - - @QtCore.Slot(str) - def update_top_label_text(self, text: str): - self.top_label.setText(text) - - @QtCore.Slot(str, str) - def append_log(self, text: str, end: str = ''): - """A slot used for receiving log info and appending it to scroll area's - content. - Args: - text (str): A log text that will append to the current one in the - scroll area. - end (str): end string which can be appended to the end of the given - line (for ex. a line break). - - Returns: - None - """ - self.log_text.setText(self.log_text.text() + text + end) - if self.is_scroll_auto: - self.scroll_bar.setValue(self.scroll_bar.maximum()) - - @QtCore.Slot(int) - def on_scroll(self, position: int): - """ - A slot for the vertical scroll bar's movement. This ensures the - auto-scrolling feature of the scroll area when the scroll bar is at its - maximum value. - - Args: - position (int): Position value of the scroll bar. - - Returns: - None - """ - if self.scroll_bar.maximum() == position: - self.is_scroll_auto = True - return - - self.is_scroll_auto = False - - @QtCore.Slot(str, int) - def fail(self, text: str, return_code: int = 1): - """ - A slot used for signals which can emit when a worker (process) has - failed. at this moment the splash screen doesn't close by itself. - it has to be closed by the user. - - Args: - text (str): A text which can be set to the top label. - - Returns: - return_code (int): Return code of the thread's code - """ - self.top_label.setText(text) - self.close_btn.show() - self.thread_return_code = return_code - self.q_thread.exit(return_code) - self.q_thread.wait() diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index e25d3479ee..1f864284cd 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -1,19 +1,6 @@ # -*- coding: utf-8 -*- # flake8: noqa E402 """AYON lib functions.""" -# add vendor to sys path based on Python version -import sys -import os -import site -from ayon_core import AYON_CORE_ROOT - -# Add Python version specific vendor folder -python_version_dir = os.path.join( - AYON_CORE_ROOT, "vendor", "python", "python_{}".format(sys.version[0]) -) -# Prepend path in sys paths -sys.path.insert(0, python_version_dir) -site.addsitedir(python_version_dir) from .local_settings import ( IniSettingRegistry, diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 3dd284b8e4..0a9d38ab65 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -281,7 +281,7 @@ class HiddenDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): kwargs["default"] = default kwargs["hidden"] = True - super(UnknownDef, self).__init__(key, **kwargs) + super(HiddenDef, self).__init__(key, **kwargs) def convert_value(self, value): return value diff --git a/client/ayon_core/modules/deadline/version.py b/client/ayon_core/modules/deadline/version.py deleted file mode 100644 index 74acd0efba..0000000000 --- a/client/ayon_core/modules/deadline/version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.1.12" diff --git a/client/ayon_core/modules/launcher_action.py b/client/ayon_core/modules/launcher_action.py index 38e88d36ca..344b0bc389 100644 --- a/client/ayon_core/modules/launcher_action.py +++ b/client/ayon_core/modules/launcher_action.py @@ -7,6 +7,7 @@ from ayon_core.addon import AYONAddon, ITrayAction class LauncherAction(AYONAddon, ITrayAction): label = "Launcher" name = "launcher_tool" + version = "1.0.0" def initialize(self, settings): diff --git a/client/ayon_core/modules/loader_action.py b/client/ayon_core/modules/loader_action.py index 1e45db05dc..a58d7fd456 100644 --- a/client/ayon_core/modules/loader_action.py +++ b/client/ayon_core/modules/loader_action.py @@ -3,6 +3,7 @@ from ayon_core.addon import AYONAddon, ITrayAddon class LoaderAddon(AYONAddon, ITrayAddon): name = "loader_tool" + version = "1.0.0" def initialize(self, settings): # Tray attributes diff --git a/client/ayon_core/modules/python_console_interpreter/addon.py b/client/ayon_core/modules/python_console_interpreter/addon.py index ffad3ce707..b0dce2585e 100644 --- a/client/ayon_core/modules/python_console_interpreter/addon.py +++ b/client/ayon_core/modules/python_console_interpreter/addon.py @@ -4,6 +4,7 @@ from ayon_core.addon import AYONAddon, ITrayAction class PythonInterpreterAction(AYONAddon, ITrayAction): label = "Console" name = "python_interpreter" + version = "1.0.0" admin_action = True def initialize(self, settings): diff --git a/client/ayon_core/modules/webserver/__init__.py b/client/ayon_core/modules/webserver/__init__.py index 0d3f767638..32f2c55f65 100644 --- a/client/ayon_core/modules/webserver/__init__.py +++ b/client/ayon_core/modules/webserver/__init__.py @@ -1,8 +1,13 @@ +from .version import __version__ +from .structures import HostMsgAction from .webserver_module import ( WebServerAddon ) __all__ = ( + "__version__", + + "HostMsgAction", "WebServerAddon", ) diff --git a/client/ayon_core/modules/webserver/host_console_listener.py b/client/ayon_core/modules/webserver/host_console_listener.py index ed8a32b9f2..2efd768e24 100644 --- a/client/ayon_core/modules/webserver/host_console_listener.py +++ b/client/ayon_core/modules/webserver/host_console_listener.py @@ -9,22 +9,18 @@ from qtpy import QtWidgets from ayon_core.addon import ITrayService from ayon_core.tools.stdout_broker.window import ConsoleDialog +from .structures import HostMsgAction + log = logging.getLogger(__name__) +# Host listener icon type class IconType: IDLE = "idle" RUNNING = "running" FAILED = "failed" -class MsgAction: - CONNECTING = "connecting" - INITIALIZED = "initialized" - ADD = "add" - CLOSE = "close" - - class HostListener: def __init__(self, webserver, module): self._window_per_id = {} @@ -96,22 +92,22 @@ class HostListener: if msg.type == aiohttp.WSMsgType.TEXT: host_name, action, text = self._parse_message(msg) - if action == MsgAction.CONNECTING: + if action == HostMsgAction.CONNECTING: self._action_per_id[host_name] = None # must be sent to main thread, or action wont trigger self.module.execute_in_main_thread( lambda: self._host_is_connecting(host_name, text)) - elif action == MsgAction.CLOSE: + elif action == HostMsgAction.CLOSE: # clean close self._close(host_name) await ws.close() - elif action == MsgAction.INITIALIZED: + elif action == HostMsgAction.INITIALIZED: self.module.execute_in_main_thread( # must be queued as _host_is_connecting might not # be triggered/finished yet lambda: self._set_host_icon(host_name, IconType.RUNNING)) - elif action == MsgAction.ADD: + elif action == HostMsgAction.ADD: self.module.execute_in_main_thread( lambda: self._add_text(host_name, text)) elif msg.type == aiohttp.WSMsgType.ERROR: diff --git a/client/ayon_core/modules/webserver/structures.py b/client/ayon_core/modules/webserver/structures.py new file mode 100644 index 0000000000..a598e3342a --- /dev/null +++ b/client/ayon_core/modules/webserver/structures.py @@ -0,0 +1,6 @@ +# Host listener message actions +class HostMsgAction: + CONNECTING = "connecting" + INITIALIZED = "initialized" + ADD = "add" + CLOSE = "close" diff --git a/client/ayon_core/modules/webserver/version.py b/client/ayon_core/modules/webserver/version.py new file mode 100644 index 0000000000..5becc17c04 --- /dev/null +++ b/client/ayon_core/modules/webserver/version.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/client/ayon_core/modules/webserver/webserver_module.py b/client/ayon_core/modules/webserver/webserver_module.py index c324e0dd18..997b6f754c 100644 --- a/client/ayon_core/modules/webserver/webserver_module.py +++ b/client/ayon_core/modules/webserver/webserver_module.py @@ -26,9 +26,12 @@ import socket from ayon_core import resources from ayon_core.addon import AYONAddon, ITrayService +from .version import __version__ + class WebServerAddon(AYONAddon, ITrayService): name = "webserver" + version = __version__ label = "WebServer" webserver_url_env = "AYON_WEBSERVER_URL" diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index c32d04c44c..8b72405048 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -11,7 +11,12 @@ from pyblish.lib import MessageHandler from ayon_core import AYON_CORE_ROOT from ayon_core.host import HostBase -from ayon_core.lib import is_in_tests, initialize_ayon_connection, emit_event +from ayon_core.lib import ( + is_in_tests, + initialize_ayon_connection, + emit_event, + version_up +) from ayon_core.addon import load_addons, AddonsManager from ayon_core.settings import get_project_settings @@ -21,6 +26,8 @@ from .template_data import get_template_data_with_names from .workfile import ( get_workdir, get_custom_workfile_template_by_string_context, + get_workfile_template_key_from_context, + get_last_workfile ) from . import ( register_loader_plugin_path, @@ -579,3 +586,48 @@ def get_process_id(): if _process_id is None: _process_id = str(uuid.uuid4()) return _process_id + + +def version_up_current_workfile(): + """Function to increment and save workfile + """ + host = registered_host() + if not host.has_unsaved_changes(): + print("No unsaved changes, skipping file save..") + return + + project_name = get_current_project_name() + folder_path = get_current_folder_path() + task_name = get_current_task_name() + host_name = get_current_host_name() + + template_key = get_workfile_template_key_from_context( + project_name, + folder_path, + task_name, + host_name, + ) + anatomy = Anatomy(project_name) + + data = get_template_data_with_names( + project_name, folder_path, task_name, host_name + ) + data["root"] = anatomy.roots + + work_template = anatomy.get_template_item("work", template_key) + + # Define saving file extension + extensions = host.get_workfile_extensions() + current_file = host.get_current_workfile() + if current_file: + extensions = [os.path.splitext(current_file)[-1]] + + work_root = work_template["directory"].format_strict(data) + file_template = work_template["file"].template + last_workfile_path = get_last_workfile( + work_root, file_template, data, extensions, True + ) + new_workfile_path = version_up(last_workfile_path) + if os.path.exists(new_workfile_path): + new_workfile_path = version_up(new_workfile_path) + host.save_workfile(new_workfile_path) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 45846553a4..0d8722dab1 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -37,6 +37,7 @@ from .creator_plugins import ( # Changes of instances and context are send as tuple of 2 information UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) +_NOT_SET = object() class UnavailableSharedData(Exception): @@ -1401,6 +1402,11 @@ class CreateContext: self._current_folder_path = None self._current_task_name = None self._current_workfile_path = None + self._current_project_settings = None + + self._current_folder_entity = _NOT_SET + self._current_task_entity = _NOT_SET + self._current_task_type = _NOT_SET self._current_project_anatomy = None @@ -1571,6 +1577,64 @@ class CreateContext: return self._current_task_name + def get_current_task_type(self): + """Task type which was used as current context on context reset. + + Returns: + Union[str, None]: Task type. + + """ + if self._current_task_type is _NOT_SET: + task_type = None + task_entity = self.get_current_task_entity() + if task_entity: + task_type = task_entity["taskType"] + self._current_task_type = task_type + return self._current_task_type + + def get_current_folder_entity(self): + """Folder entity for current context folder. + + Returns: + Union[dict[str, Any], None]: Folder entity. + + """ + if self._current_folder_entity is not _NOT_SET: + return copy.deepcopy(self._current_folder_entity) + folder_entity = None + folder_path = self.get_current_folder_path() + if folder_path: + project_name = self.get_current_project_name() + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + self._current_folder_entity = folder_entity + return copy.deepcopy(self._current_folder_entity) + + def get_current_task_entity(self): + """Task entity for current context task. + + Returns: + Union[dict[str, Any], None]: Task entity. + + """ + if self._current_task_entity is not _NOT_SET: + return copy.deepcopy(self._current_task_entity) + task_entity = None + task_name = self.get_current_task_name() + if task_name: + folder_entity = self.get_current_folder_entity() + if folder_entity: + project_name = self.get_current_project_name() + task_entity = ayon_api.get_task_by_name( + project_name, + folder_id=folder_entity["id"], + task_name=task_name + ) + self._current_task_entity = task_entity + return copy.deepcopy(self._current_task_entity) + + def get_current_workfile_path(self): """Workfile path which was opened on context reset. @@ -1592,6 +1656,12 @@ class CreateContext: self._current_project_name) return self._current_project_anatomy + def get_current_project_settings(self): + if self._current_project_settings is None: + self._current_project_settings = get_project_settings( + self.get_current_project_name()) + return self._current_project_settings + @property def context_has_changed(self): """Host context has changed. @@ -1718,7 +1788,12 @@ class CreateContext: self._current_task_name = task_name self._current_workfile_path = workfile_path + self._current_folder_entity = _NOT_SET + self._current_task_entity = _NOT_SET + self._current_task_type = _NOT_SET + self._current_project_anatomy = None + self._current_project_settings = None def reset_plugins(self, discover_publish_plugins=True): """Reload plugins. @@ -1772,7 +1847,7 @@ class CreateContext: def _reset_creator_plugins(self): # Prepare settings - project_settings = get_project_settings(self.project_name) + project_settings = self.get_current_project_settings() # Discover and prepare creators creators = {} diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 8d3644637b..7f63089d33 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -336,17 +336,16 @@ def get_plugin_settings(plugin, project_settings, log, category=None): settings_category = getattr(plugin, "settings_category", None) if settings_category: try: - return ( - project_settings - [settings_category] - ["publish"] - [plugin.__name__] - ) + category_settings = project_settings[settings_category] except KeyError: log.warning(( - "Couldn't find plugin '{}' settings" - " under settings category '{}'" - ).format(plugin.__name__, settings_category)) + "Couldn't find settings category '{}' in project settings" + ).format(settings_category)) + return {} + + try: + return category_settings["publish"][plugin.__name__] + except KeyError: return {} # Use project settings based on a category name diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index ad5a5d43fc..b6636696c1 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -313,7 +313,14 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): # Define version version_number = None - if self.follow_workfile_version: + + # Allow an instance to force enable or disable the version + # following of the current context + use_context_version = self.follow_workfile_version + if "followWorkfileVersion" in instance.data: + use_context_version = instance.data["followWorkfileVersion"] + + if use_context_version: version_number = context.data("version") # Even if 'follow_workfile_version' is enabled, it may not be set @@ -391,7 +398,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): anatomy_data.update(folder_data) return - if instance.data.get("newAssetPublishing"): + if ( + instance.data.get("newHierarchyIntegration") + # Backwards compatible (Deprecated since 24/06/06) + or instance.data.get("newAssetPublishing") + ): hierarchy = instance.data["hierarchy"] anatomy_data["hierarchy"] = hierarchy @@ -409,7 +420,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): "path": instance.data["folderPath"], # TODO get folder type from hierarchy # Using 'Shot' is current default behavior of editorial - # (or 'newAssetPublishing') publishing. + # (or 'newHierarchyIntegration') publishing. "type": "Shot", }, }) @@ -432,15 +443,22 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): if task_data: # Fill task data # - if we're in editorial, make sure the task type is filled - if ( - not instance.data.get("newAssetPublishing") - or task_data["type"] - ): + new_hierarchy = ( + instance.data.get("newHierarchyIntegration") + # Backwards compatible (Deprecated since 24/06/06) + or instance.data.get("newAssetPublishing") + ) + if not new_hierarchy or task_data["type"]: anatomy_data["task"] = task_data return # New hierarchy is not created, so we can only skip rest of the logic - if not instance.data.get("newAssetPublishing"): + new_hierarchy = ( + instance.data.get("newHierarchyIntegration") + # Backwards compatible (Deprecated since 24/06/06) + or instance.data.get("newAssetPublishing") + ) + if not new_hierarchy: return # Try to find task data based on hierarchy context and folder path diff --git a/client/ayon_core/plugins/publish/collect_farm_target.py b/client/ayon_core/plugins/publish/collect_farm_target.py index 3bf89450ec..e0edd795d8 100644 --- a/client/ayon_core/plugins/publish/collect_farm_target.py +++ b/client/ayon_core/plugins/publish/collect_farm_target.py @@ -14,22 +14,20 @@ class CollectFarmTarget(pyblish.api.InstancePlugin): if not instance.data.get("farm"): return - context = instance.context + addons_manager = instance.context.data.get("ayonAddonsManager") - farm_name = "" - addons_manager = context.data.get("ayonAddonsManager") - - for farm_renderer in ["deadline", "royalrender"]: - addon = addons_manager.get(farm_renderer, False) - - if not addon: - self.log.error("Cannot find AYON addon '{0}'.".format( - farm_renderer)) - elif addon.enabled: + farm_renderer_addons = ["deadline", "royalrender"] + for farm_renderer in farm_renderer_addons: + addon = addons_manager.get(farm_renderer) + if addon and addon.enabled: farm_name = farm_renderer - - if farm_name: - self.log.debug("Collected render target: {0}".format(farm_name)) - instance.data["toBeRenderedOn"] = farm_name + break else: - AssertionError("No AYON renderer addon found") + # No enabled farm render addon found, then report all farm + # addons that were searched for yet not found + for farm_renderer in farm_renderer_addons: + self.log.error(f"Cannot find AYON addon '{farm_renderer}'.") + raise RuntimeError("No AYON renderer addon found.") + + self.log.debug("Collected render target: {0}".format(farm_name)) + instance.data["toBeRenderedOn"] = farm_name diff --git a/client/ayon_core/plugins/publish/collect_scene_version.py b/client/ayon_core/plugins/publish/collect_scene_version.py index b04900c74e..ea4823d62a 100644 --- a/client/ayon_core/plugins/publish/collect_scene_version.py +++ b/client/ayon_core/plugins/publish/collect_scene_version.py @@ -27,7 +27,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): "nuke", "photoshop", "resolve", - "tvpaint" + "tvpaint", + "motionbuilder", + "substancepainter" ] # in some cases of headless publishing (for example webpublisher using PS) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 1130c575a3..a28a761e7e 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -202,43 +202,16 @@ class ExtractOIIOTranscode(publish.Extractor): added_representations = True if added_representations: - self._mark_original_repre_for_deletion(repre, profile, - added_review) + self._mark_original_repre_for_deletion( + repre, profile, added_review + ) - for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) instance.data["representations"].extend(new_representations) - def _rename_in_representation(self, new_repre, files_to_convert, - output_name, output_extension): - """Replace old extension with new one everywhere in representation. - - Args: - new_repre (dict) - files_to_convert (list): of filenames from repre["files"], - standardized to always list - output_name (str): key of output definition from Settings, - if "" token used, keep original repre name - output_extension (str): extension from output definition - """ - if output_name != "passthrough": - new_repre["name"] = output_name - if not output_extension: - return - - new_repre["ext"] = output_extension - - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(file_name) - new_repre["files"] = renamed_files - def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): """Replace old extension with new one everywhere in representation. @@ -364,7 +337,7 @@ class ExtractOIIOTranscode(publish.Extractor): if not repre.get("colorspaceData"): self.log.debug("Representation '{}' has no colorspace data. " - "Skipped.") + "Skipped.".format(repre["name"])) return False return True diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 865b566e6e..1a4cda4dbb 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -380,29 +380,28 @@ class IntegrateAsset(pyblish.api.InstancePlugin): data = { "families": get_instance_families(instance) } - attribibutes = {} + attributes = {} product_group = instance.data.get("productGroup") if product_group: - attribibutes["productGroup"] = product_group + attributes["productGroup"] = product_group elif existing_product_entity: # Preserve previous product group if new version does not set it product_group = existing_product_entity.get("attrib", {}).get( "productGroup" ) if product_group is not None: - attribibutes["productGroup"] = product_group + attributes["productGroup"] = product_group product_id = None if existing_product_entity: product_id = existing_product_entity["id"] - product_entity = new_product_entity( product_name, product_type, folder_entity["id"], data=data, - attribs=attribibutes, + attribs=attributes, entity_id=product_id ) @@ -464,6 +463,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): version_number, product_entity["id"], task_id=task_id, + status=instance.data.get("status"), data=version_data, attribs=version_attributes, entity_id=version_id, diff --git a/client/ayon_core/plugins/publish/validate_asset_docs.py b/client/ayon_core/plugins/publish/validate_asset_docs.py index 95fe4252be..b80b81b366 100644 --- a/client/ayon_core/plugins/publish/validate_asset_docs.py +++ b/client/ayon_core/plugins/publish/validate_asset_docs.py @@ -24,7 +24,11 @@ class ValidateFolderEntities(pyblish.api.InstancePlugin): if instance.data.get("folderEntity"): self.log.debug("Instance has set fodler entity in its data.") - elif instance.data.get("newAssetPublishing"): + elif ( + instance.data.get("newHierarchyIntegration") + # Backwards compatible (Deprecated since 24/06/06) + or instance.data.get("newAssetPublishing") + ): # skip if it is editorial self.log.debug("Editorial instance has no need to check...") diff --git a/client/ayon_core/resources/app_icons/motionbuilder.png b/client/ayon_core/resources/app_icons/motionbuilder.png new file mode 100644 index 0000000000..68a17f7afb Binary files /dev/null and b/client/ayon_core/resources/app_icons/motionbuilder.png differ diff --git a/client/ayon_core/resources/ftrack/action_icons/ActionAskWhereIRun.svg b/client/ayon_core/resources/ftrack/action_icons/ActionAskWhereIRun.svg deleted file mode 100644 index c02b8f83d8..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/ActionAskWhereIRun.svg +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/AssetsRemover.svg b/client/ayon_core/resources/ftrack/action_icons/AssetsRemover.svg deleted file mode 100644 index e838ee9f28..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/AssetsRemover.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/ayon_core/resources/ftrack/action_icons/BatchTasks.svg b/client/ayon_core/resources/ftrack/action_icons/BatchTasks.svg deleted file mode 100644 index 5cf5d423dd..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/BatchTasks.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/ComponentOpen.svg b/client/ayon_core/resources/ftrack/action_icons/ComponentOpen.svg deleted file mode 100644 index f549e6142b..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/ComponentOpen.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/CreateFolders.svg b/client/ayon_core/resources/ftrack/action_icons/CreateFolders.svg deleted file mode 100644 index 18efc273aa..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/CreateFolders.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/CreateProjectFolders.svg b/client/ayon_core/resources/ftrack/action_icons/CreateProjectFolders.svg deleted file mode 100644 index 0e5821b0be..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/CreateProjectFolders.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/DeleteAsset.svg b/client/ayon_core/resources/ftrack/action_icons/DeleteAsset.svg deleted file mode 100644 index 855bdae7c5..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/DeleteAsset.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/Delivery.svg b/client/ayon_core/resources/ftrack/action_icons/Delivery.svg deleted file mode 100644 index a6333333ae..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/Delivery.svg +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/MultipleNotes.svg b/client/ayon_core/resources/ftrack/action_icons/MultipleNotes.svg deleted file mode 100644 index 40113fc709..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/MultipleNotes.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/OpenPypeAdmin.svg b/client/ayon_core/resources/ftrack/action_icons/OpenPypeAdmin.svg deleted file mode 100644 index c2abc6146f..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/OpenPypeAdmin.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/PrepareProject.svg b/client/ayon_core/resources/ftrack/action_icons/PrepareProject.svg deleted file mode 100644 index 644d83f84d..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/PrepareProject.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/RV.png b/client/ayon_core/resources/ftrack/action_icons/RV.png deleted file mode 100644 index 741e7a9772..0000000000 Binary files a/client/ayon_core/resources/ftrack/action_icons/RV.png and /dev/null differ diff --git a/client/ayon_core/resources/ftrack/action_icons/SeedProject.svg b/client/ayon_core/resources/ftrack/action_icons/SeedProject.svg deleted file mode 100644 index ff818b5ecb..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/SeedProject.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/SortReview.svg b/client/ayon_core/resources/ftrack/action_icons/SortReview.svg deleted file mode 100644 index 13a7def648..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/SortReview.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/TestAction.svg b/client/ayon_core/resources/ftrack/action_icons/TestAction.svg deleted file mode 100644 index 917ef2d0c7..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/TestAction.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/action_icons/Thumbnail.svg b/client/ayon_core/resources/ftrack/action_icons/Thumbnail.svg deleted file mode 100644 index 9af330e79a..0000000000 --- a/client/ayon_core/resources/ftrack/action_icons/Thumbnail.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/ayon_core/resources/ftrack/sign_in_message.html b/client/ayon_core/resources/ftrack/sign_in_message.html deleted file mode 100644 index 8ee2828c26..0000000000 --- a/client/ayon_core/resources/ftrack/sign_in_message.html +++ /dev/null @@ -1,32 +0,0 @@ - - - -

Sign in to Ftrack was successful

-

- You signed in with username {}. -

-

- You can close this window now. -

- - diff --git a/client/ayon_core/tools/adobe_webserver/readme.txt b/client/ayon_core/tools/adobe_webserver/readme.txt deleted file mode 100644 index d02d390277..0000000000 --- a/client/ayon_core/tools/adobe_webserver/readme.txt +++ /dev/null @@ -1,12 +0,0 @@ -Adobe webserver ---------------- -Aiohttp (Asyncio) based websocket server used for communication with host -applications, currently only for Adobe (but could be used for any non python -DCC which has websocket client). - -This webserver is started in spawned Python process that opens DCC during -its launch, waits for connection from DCC and handles communication going -forward. Server is closed before Python process is killed. - -(Different from `ayon_core/modules/webserver` as that one is running in Tray, -this one is running in spawn Python process.) \ No newline at end of file diff --git a/client/ayon_core/tools/launcher/ui/hierarchy_page.py b/client/ayon_core/tools/launcher/ui/hierarchy_page.py index 226a57930b..ad48e8ac77 100644 --- a/client/ayon_core/tools/launcher/ui/hierarchy_page.py +++ b/client/ayon_core/tools/launcher/ui/hierarchy_page.py @@ -70,7 +70,7 @@ class HierarchyPage(QtWidgets.QWidget): main_layout.addWidget(content_body, 1) btn_back.clicked.connect(self._on_back_clicked) - refresh_btn.clicked.connect(self._on_refreh_clicked) + refresh_btn.clicked.connect(self._on_refresh_clicked) folders_filter_text.textChanged.connect(self._on_filter_text_changed) self._is_visible = False @@ -99,7 +99,7 @@ class HierarchyPage(QtWidgets.QWidget): def _on_back_clicked(self): self._controller.set_selected_project(None) - def _on_refreh_clicked(self): + def _on_refresh_clicked(self): self._controller.refresh() def _on_filter_text_changed(self, text): diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 509db4d037..a1c1e6a062 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -172,12 +172,30 @@ class VersionItem: def __gt__(self, other): if not isinstance(other, VersionItem): return False - if ( - other.version == self.version - and self.is_hero - ): + # Make sure hero versions are positive + version = abs(self.version) + other_version = abs(other.version) + # Hero version is greater than non-hero + if version == other_version: + return self.is_hero + return version > other_version + + def __lt__(self, other): + if not isinstance(other, VersionItem): return True - return other.version < self.version + # Make sure hero versions are positive + version = abs(self.version) + other_version = abs(other.version) + # Non-hero version is lesser than hero + if version == other_version: + return not self.is_hero + return version < other_version + + def __ge__(self, other): + return self.__eq__(other) or self.__gt__(other) + + def __le__(self, other): + return self.__eq__(other) or self.__lt__(other) def to_data(self): return { diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 35188369c2..9fead226f0 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -348,10 +348,18 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): return set() if not self._loaded_products_cache.is_valid: - if isinstance(self._host, ILoadHost): - containers = self._host.get_containers() - else: - containers = self._host.ls() + try: + if isinstance(self._host, ILoadHost): + containers = self._host.get_containers() + else: + containers = self._host.ls() + + except BaseException: + self.log.error( + "Failed to collect loaded products.", exc_info=True + ) + containers = [] + repre_ids = set() for container in containers: repre_id = container.get("representation") diff --git a/client/ayon_core/tools/loader/ui/folders_widget.py b/client/ayon_core/tools/loader/ui/folders_widget.py index 7b146456da..efd041937d 100644 --- a/client/ayon_core/tools/loader/ui/folders_widget.py +++ b/client/ayon_core/tools/loader/ui/folders_widget.py @@ -321,6 +321,8 @@ class LoaderFoldersWidget(QtWidgets.QWidget): """ self._folders_proxy_model.setFilterFixedString(name) + if name: + self._folders_view.expandAll() def set_merged_products_selection(self, items): """ diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 8035b1f0fe..7b9124608b 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -199,7 +199,9 @@ class ProductsModel(QtGui.QStandardItemModel): product_item = self._product_items_by_id.get(product_id) if product_item is None: return None - return list(product_item.version_items.values()) + product_items = list(product_item.version_items.values()) + product_items.sort(reverse=True) + return product_items if role == QtCore.Qt.EditRole: return None diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index ede772b917..4e2cfd8783 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -8,6 +8,7 @@ import tempfile import shutil import inspect from abc import ABCMeta, abstractmethod +import re import six import arrow @@ -39,6 +40,7 @@ from ayon_core.pipeline.create.context import ( ) from ayon_core.pipeline.publish import get_publish_instance_label from ayon_core.tools.common_models import HierarchyModel +from ayon_core.lib.profiles_filtering import filter_profiles # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -1686,6 +1688,15 @@ class PublisherController(BasePublisherController): """Publish plugins.""" return self._create_context.publish_plugins + def _get_current_project_settings(self): + """Current project settings. + + Returns: + dict + """ + + return self._create_context.get_current_project_settings() + # Hierarchy model def get_folder_items(self, project_name, sender=None): return self._hierarchy_model.get_folder_items(project_name, sender) @@ -1827,8 +1838,13 @@ class PublisherController(BasePublisherController): def _collect_creator_items(self): # TODO add crashed initialization of create plugins to report output = {} + allowed_creator_pattern = self._get_allowed_creators_pattern() for identifier, creator in self._create_context.creators.items(): try: + if (not self._is_label_allowed( + creator.label, allowed_creator_pattern)): + self.log.debug(f"{creator.label} not allowed for context") + continue output[identifier] = CreatorItem.from_creator(creator) except Exception: self.log.error( @@ -1839,6 +1855,60 @@ class PublisherController(BasePublisherController): return output + def _get_allowed_creators_pattern(self): + """Provide regex pattern for configured creator labels in this context + + If no profile matches current context, it shows all creators. + Support usage of regular expressions for configured values. + Returns: + (re.Pattern)[optional]: None or regex compiled patterns + into single one ('Render|Image.*') + """ + + task_type = self._create_context.get_current_task_type() + project_settings = self._get_current_project_settings() + + filter_creator_profiles = ( + project_settings + ["core"] + ["tools"] + ["creator"] + ["filter_creator_profiles"] + ) + filtering_criteria = { + "task_names": self.current_task_name, + "task_types": task_type, + "host_names": self._create_context.host_name + } + profile = filter_profiles( + filter_creator_profiles, + filtering_criteria, + logger=self.log + ) + + allowed_creator_pattern = None + if profile: + allowed_creator_labels = { + label + for label in profile["creator_labels"] + if label + } + self.log.debug(f"Only allowed `{allowed_creator_labels}` creators") + allowed_creator_pattern = ( + re.compile("|".join(allowed_creator_labels))) + return allowed_creator_pattern + + def _is_label_allowed(self, label, allowed_labels_regex): + """Implement regex support for allowed labels. + + Args: + label (str): Label of creator - shown in Publisher + allowed_labels_regex (re.Pattern): compiled regular expression + """ + if not allowed_labels_regex: + return True + return bool(allowed_labels_regex.match(label)) + def _reset_instances(self): """Reset create instances.""" if self._resetting_instances: diff --git a/client/ayon_core/tools/stdout_broker/__init__.py b/client/ayon_core/tools/stdout_broker/__init__.py index e69de29bb2..e104c60573 100644 --- a/client/ayon_core/tools/stdout_broker/__init__.py +++ b/client/ayon_core/tools/stdout_broker/__init__.py @@ -0,0 +1,5 @@ +from .broker import StdOutBroker + +__all__ = ( + "StdOutBroker", +) diff --git a/client/ayon_core/tools/stdout_broker/app.py b/client/ayon_core/tools/stdout_broker/app.py index 15447b608b..ae73db1bb9 100644 --- a/client/ayon_core/tools/stdout_broker/app.py +++ b/client/ayon_core/tools/stdout_broker/app.py @@ -1,173 +1,12 @@ -import os -import sys -import threading -import collections -import websocket -import json -from datetime import datetime +import warnings +from .broker import StdOutBroker -from ayon_core.lib import Logger -from openpype_modules.webserver.host_console_listener import MsgAction +warnings.warn( + ( + "Import of 'StdOutBroker' from 'ayon_core.tools.stdout_broker.app'" + " is deprecated. Please use 'ayon_core.tools.stdout_broker' instead." + ), + DeprecationWarning +) -log = Logger.get_logger(__name__) - - -class StdOutBroker: - """ - Application showing console in Services tray for non python hosts - instead of cmd window. - """ - MAX_LINES = 10000 - TIMER_TIMEOUT = 0.200 - - def __init__(self, host_name): - self.host_name = host_name - self.webserver_client = None - - self.original_stdout_write = None - self.original_stderr_write = None - self.log_queue = collections.deque() - - date_str = datetime.now().strftime("%d%m%Y%H%M%S") - self.host_id = "{}_{}".format(self.host_name, date_str) - - self._std_available = False - self._is_running = False - self._catch_std_outputs() - - self._timer = None - - @property - def send_to_tray(self): - """Checks if connected to tray and have access to logs.""" - return self.webserver_client and self._std_available - - def start(self): - """Start app, create and start timer""" - if not self._std_available or self._is_running: - return - self._is_running = True - self._create_timer() - self._connect_to_tray() - - def stop(self): - """Disconnect from Tray, process last logs""" - if not self._is_running: - return - self._is_running = False - self._process_queue() - self._disconnect_from_tray() - - def host_connected(self): - """Send to Tray console that host is ready - icon change. """ - log.info("Host {} connected".format(self.host_id)) - - payload = { - "host": self.host_id, - "action": MsgAction.INITIALIZED, - "text": "Integration with {}".format( - str.capitalize(self.host_name)) - } - self._send(payload) - - def _create_timer(self): - timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback) - timer.start() - self._timer = timer - - def _timer_callback(self): - if not self._is_running: - return - self._process_queue() - self._create_timer() - - def _connect_to_tray(self): - """Connect to Tray webserver to pass console output. """ - if not self._std_available: # not content to log - return - ws = websocket.WebSocket() - webserver_url = os.environ.get("AYON_WEBSERVER_URL") - - if not webserver_url: - print("Unknown webserver url, cannot connect to pass log") - return - - webserver_url = webserver_url.replace("http", "ws") - ws.connect("{}/ws/host_listener".format(webserver_url)) - self.webserver_client = ws - - payload = { - "host": self.host_id, - "action": MsgAction.CONNECTING, - "text": "Integration with {}".format( - str.capitalize(self.host_name)) - } - self._send(payload) - - def _disconnect_from_tray(self): - """Send to Tray that host is closing - remove from Services. """ - print("Host {} closing".format(self.host_name)) - if not self.webserver_client: - return - - payload = { - "host": self.host_id, - "action": MsgAction.CLOSE, - "text": "Integration with {}".format( - str.capitalize(self.host_name)) - } - - self._send(payload) - self.webserver_client.close() - - def _catch_std_outputs(self): - """Redirects standard out and error to own functions""" - if sys.stdout: - self.original_stdout_write = sys.stdout.write - sys.stdout.write = self._my_stdout_write - self._std_available = True - - if sys.stderr: - self.original_stderr_write = sys.stderr.write - sys.stderr.write = self._my_stderr_write - self._std_available = True - - def _my_stdout_write(self, text): - """Appends outputted text to queue, keep writing to original stdout""" - if self.original_stdout_write is not None: - self.original_stdout_write(text) - if self.send_to_tray: - self.log_queue.append(text) - - def _my_stderr_write(self, text): - """Appends outputted text to queue, keep writing to original stderr""" - if self.original_stderr_write is not None: - self.original_stderr_write(text) - if self.send_to_tray: - self.log_queue.append(text) - - def _process_queue(self): - """Sends lines and purges queue""" - if not self.send_to_tray: - return - - lines = tuple(self.log_queue) - self.log_queue.clear() - if lines: - payload = { - "host": self.host_id, - "action": MsgAction.ADD, - "text": "\n".join(lines) - } - - self._send(payload) - - def _send(self, payload): - """Worker method to send to existing websocket connection.""" - if not self.send_to_tray: - return - - try: - self.webserver_client.send(json.dumps(payload)) - except ConnectionResetError: # Tray closed - self._connect_to_tray() +__all__ = ("StdOutBroker", ) diff --git a/client/ayon_core/tools/stdout_broker/broker.py b/client/ayon_core/tools/stdout_broker/broker.py new file mode 100644 index 0000000000..291936008b --- /dev/null +++ b/client/ayon_core/tools/stdout_broker/broker.py @@ -0,0 +1,174 @@ +import os +import sys +import threading +import collections +import json +from datetime import datetime + +import websocket + +from ayon_core.lib import Logger +from ayon_core.modules.webserver import HostMsgAction + +log = Logger.get_logger(__name__) + + +class StdOutBroker: + """ + Application showing console in Services tray for non python hosts + instead of cmd window. + """ + MAX_LINES = 10000 + TIMER_TIMEOUT = 0.200 + + def __init__(self, host_name): + self.host_name = host_name + self.webserver_client = None + + self.original_stdout_write = None + self.original_stderr_write = None + self.log_queue = collections.deque() + + date_str = datetime.now().strftime("%d%m%Y%H%M%S") + self.host_id = "{}_{}".format(self.host_name, date_str) + + self._std_available = False + self._is_running = False + self._catch_std_outputs() + + self._timer = None + + @property + def send_to_tray(self): + """Checks if connected to tray and have access to logs.""" + return self.webserver_client and self._std_available + + def start(self): + """Start app, create and start timer""" + if not self._std_available or self._is_running: + return + self._is_running = True + self._create_timer() + self._connect_to_tray() + + def stop(self): + """Disconnect from Tray, process last logs""" + if not self._is_running: + return + self._is_running = False + self._process_queue() + self._disconnect_from_tray() + + def host_connected(self): + """Send to Tray console that host is ready - icon change. """ + log.info("Host {} connected".format(self.host_id)) + + payload = { + "host": self.host_id, + "action": HostMsgAction.INITIALIZED, + "text": "Integration with {}".format( + str.capitalize(self.host_name)) + } + self._send(payload) + + def _create_timer(self): + timer = threading.Timer(self.TIMER_TIMEOUT, self._timer_callback) + timer.start() + self._timer = timer + + def _timer_callback(self): + if not self._is_running: + return + self._process_queue() + self._create_timer() + + def _connect_to_tray(self): + """Connect to Tray webserver to pass console output. """ + if not self._std_available: # not content to log + return + ws = websocket.WebSocket() + webserver_url = os.environ.get("AYON_WEBSERVER_URL") + + if not webserver_url: + print("Unknown webserver url, cannot connect to pass log") + return + + webserver_url = webserver_url.replace("http", "ws") + ws.connect("{}/ws/host_listener".format(webserver_url)) + self.webserver_client = ws + + payload = { + "host": self.host_id, + "action": HostMsgAction.CONNECTING, + "text": "Integration with {}".format( + str.capitalize(self.host_name)) + } + self._send(payload) + + def _disconnect_from_tray(self): + """Send to Tray that host is closing - remove from Services. """ + print("Host {} closing".format(self.host_name)) + if not self.webserver_client: + return + + payload = { + "host": self.host_id, + "action": HostMsgAction.CLOSE, + "text": "Integration with {}".format( + str.capitalize(self.host_name)) + } + + self._send(payload) + self.webserver_client.close() + + def _catch_std_outputs(self): + """Redirects standard out and error to own functions""" + if sys.stdout: + self.original_stdout_write = sys.stdout.write + sys.stdout.write = self._my_stdout_write + self._std_available = True + + if sys.stderr: + self.original_stderr_write = sys.stderr.write + sys.stderr.write = self._my_stderr_write + self._std_available = True + + def _my_stdout_write(self, text): + """Appends outputted text to queue, keep writing to original stdout""" + if self.original_stdout_write is not None: + self.original_stdout_write(text) + if self.send_to_tray: + self.log_queue.append(text) + + def _my_stderr_write(self, text): + """Appends outputted text to queue, keep writing to original stderr""" + if self.original_stderr_write is not None: + self.original_stderr_write(text) + if self.send_to_tray: + self.log_queue.append(text) + + def _process_queue(self): + """Sends lines and purges queue""" + if not self.send_to_tray: + return + + lines = tuple(self.log_queue) + self.log_queue.clear() + if lines: + payload = { + "host": self.host_id, + "action": HostMsgAction.ADD, + "text": "\n".join(lines) + } + + self._send(payload) + + def _send(self, payload): + """Worker method to send to existing websocket connection.""" + if not self.send_to_tray: + return + + try: + self.webserver_client.send(json.dumps(payload)) + except ConnectionResetError: # Tray closed + self._connect_to_tray() diff --git a/client/ayon_core/tools/tray/tray.py b/client/ayon_core/tools/tray/tray.py index 957518afe4..eca87eb11d 100644 --- a/client/ayon_core/tools/tray/tray.py +++ b/client/ayon_core/tools/tray/tray.py @@ -447,8 +447,10 @@ class SystemTrayIcon(QtWidgets.QSystemTrayIcon): def initialize_addons(self): self._initializing_addons = True - self.tray_man.initialize_addons() - self._initializing_addons = False + try: + self.tray_man.initialize_addons() + finally: + self._initializing_addons = False def _click_timer_timeout(self): self._click_timer.stop() diff --git a/client/ayon_core/tools/utils/folders_widget.py b/client/ayon_core/tools/utils/folders_widget.py index 2ad640de37..6aae68bf8a 100644 --- a/client/ayon_core/tools/utils/folders_widget.py +++ b/client/ayon_core/tools/utils/folders_widget.py @@ -370,6 +370,8 @@ class FoldersWidget(QtWidgets.QWidget): """ self._folders_proxy_model.setFilterFixedString(name) + if name: + self._folders_view.expandAll() def refresh(self): """Refresh folders model. diff --git a/client/ayon_core/vendor/python/common/README.md b/client/ayon_core/vendor/python/README.md similarity index 100% rename from client/ayon_core/vendor/python/common/README.md rename to client/ayon_core/vendor/python/README.md diff --git a/client/ayon_core/vendor/python/python_2/README.md b/client/ayon_core/vendor/python/python_2/README.md deleted file mode 100644 index f101ddbf54..0000000000 --- a/client/ayon_core/vendor/python/python_2/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## Info - -Only **Python 2** specific modules are here. \ No newline at end of file diff --git a/client/ayon_core/vendor/python/python_2/arrow/__init__.py b/client/ayon_core/vendor/python/python_2/arrow/__init__.py deleted file mode 100644 index 2883527be8..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -from ._version import __version__ -from .api import get, now, utcnow -from .arrow import Arrow -from .factory import ArrowFactory -from .formatter import ( - FORMAT_ATOM, - FORMAT_COOKIE, - FORMAT_RFC822, - FORMAT_RFC850, - FORMAT_RFC1036, - FORMAT_RFC1123, - FORMAT_RFC2822, - FORMAT_RFC3339, - FORMAT_RSS, - FORMAT_W3C, -) -from .parser import ParserError diff --git a/client/ayon_core/vendor/python/python_2/arrow/_version.py b/client/ayon_core/vendor/python/python_2/arrow/_version.py deleted file mode 100644 index fd86b3ee91..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.17.0" diff --git a/client/ayon_core/vendor/python/python_2/arrow/api.py b/client/ayon_core/vendor/python/python_2/arrow/api.py deleted file mode 100644 index a6b7be3de2..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/api.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Provides the default implementation of :class:`ArrowFactory ` -methods for use as a module API. - -""" - -from __future__ import absolute_import - -from arrow.factory import ArrowFactory - -# internal default factory. -_factory = ArrowFactory() - - -def get(*args, **kwargs): - """Calls the default :class:`ArrowFactory ` ``get`` method.""" - - return _factory.get(*args, **kwargs) - - -get.__doc__ = _factory.get.__doc__ - - -def utcnow(): - """Calls the default :class:`ArrowFactory ` ``utcnow`` method.""" - - return _factory.utcnow() - - -utcnow.__doc__ = _factory.utcnow.__doc__ - - -def now(tz=None): - """Calls the default :class:`ArrowFactory ` ``now`` method.""" - - return _factory.now(tz) - - -now.__doc__ = _factory.now.__doc__ - - -def factory(type): - """Returns an :class:`.ArrowFactory` for the specified :class:`Arrow ` - or derived type. - - :param type: the type, :class:`Arrow ` or derived. - - """ - - return ArrowFactory(type) - - -__all__ = ["get", "utcnow", "now", "factory"] diff --git a/client/ayon_core/vendor/python/python_2/arrow/arrow.py b/client/ayon_core/vendor/python/python_2/arrow/arrow.py deleted file mode 100644 index 4fe9541789..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/arrow.py +++ /dev/null @@ -1,1584 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Provides the :class:`Arrow ` class, an enhanced ``datetime`` -replacement. - -""" - -from __future__ import absolute_import - -import calendar -import sys -import warnings -from datetime import datetime, timedelta -from datetime import tzinfo as dt_tzinfo -from math import trunc - -from dateutil import tz as dateutil_tz -from dateutil.relativedelta import relativedelta - -from arrow import formatter, locales, parser, util - -if sys.version_info[:2] < (3, 6): # pragma: no cover - with warnings.catch_warnings(): - warnings.simplefilter("default", DeprecationWarning) - warnings.warn( - "Arrow will drop support for Python 2.7 and 3.5 in the upcoming v1.0.0 release. Please upgrade to " - "Python 3.6+ to continue receiving updates for Arrow.", - DeprecationWarning, - ) - - -class Arrow(object): - """An :class:`Arrow ` object. - - Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing - additional functionality. - - :param year: the calendar year. - :param month: the calendar month. - :param day: the calendar day. - :param hour: (optional) the hour. Defaults to 0. - :param minute: (optional) the minute, Defaults to 0. - :param second: (optional) the second, Defaults to 0. - :param microsecond: (optional) the microsecond. Defaults to 0. - :param tzinfo: (optional) A timezone expression. Defaults to UTC. - :param fold: (optional) 0 or 1, used to disambiguate repeated times. Defaults to 0. - - .. _tz-expr: - - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO 8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - - Usage:: - - >>> import arrow - >>> arrow.Arrow(2013, 5, 5, 12, 30, 45) - - - """ - - resolution = datetime.resolution - - _ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"] - _ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS] - _MONTHS_PER_QUARTER = 3 - _SECS_PER_MINUTE = float(60) - _SECS_PER_HOUR = float(60 * 60) - _SECS_PER_DAY = float(60 * 60 * 24) - _SECS_PER_WEEK = float(60 * 60 * 24 * 7) - _SECS_PER_MONTH = float(60 * 60 * 24 * 30.5) - _SECS_PER_YEAR = float(60 * 60 * 24 * 365.25) - - def __init__( - self, - year, - month, - day, - hour=0, - minute=0, - second=0, - microsecond=0, - tzinfo=None, - **kwargs - ): - if tzinfo is None: - tzinfo = dateutil_tz.tzutc() - # detect that tzinfo is a pytz object (issue #626) - elif ( - isinstance(tzinfo, dt_tzinfo) - and hasattr(tzinfo, "localize") - and hasattr(tzinfo, "zone") - and tzinfo.zone - ): - tzinfo = parser.TzinfoParser.parse(tzinfo.zone) - elif util.isstr(tzinfo): - tzinfo = parser.TzinfoParser.parse(tzinfo) - - fold = kwargs.get("fold", 0) - - # use enfold here to cover direct arrow.Arrow init on 2.7/3.5 - self._datetime = dateutil_tz.enfold( - datetime(year, month, day, hour, minute, second, microsecond, tzinfo), - fold=fold, - ) - - # factories: single object, both original and from datetime. - - @classmethod - def now(cls, tzinfo=None): - """Constructs an :class:`Arrow ` object, representing "now" in the given - timezone. - - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. - - Usage:: - - >>> arrow.now('Asia/Baku') - - - """ - - if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() - - dt = datetime.now(tzinfo) - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dt.tzinfo, - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def utcnow(cls): - """Constructs an :class:`Arrow ` object, representing "now" in UTC - time. - - Usage:: - - >>> arrow.utcnow() - - - """ - - dt = datetime.now(dateutil_tz.tzutc()) - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dt.tzinfo, - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def fromtimestamp(cls, timestamp, tzinfo=None): - """Constructs an :class:`Arrow ` object from a timestamp, converted to - the given timezone. - - :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. - :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. - """ - - if tzinfo is None: - tzinfo = dateutil_tz.tzlocal() - elif util.isstr(tzinfo): - tzinfo = parser.TzinfoParser.parse(tzinfo) - - if not util.is_timestamp(timestamp): - raise ValueError( - "The provided timestamp '{}' is invalid.".format(timestamp) - ) - - timestamp = util.normalize_timestamp(float(timestamp)) - dt = datetime.fromtimestamp(timestamp, tzinfo) - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dt.tzinfo, - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def utcfromtimestamp(cls, timestamp): - """Constructs an :class:`Arrow ` object from a timestamp, in UTC time. - - :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. - - """ - - if not util.is_timestamp(timestamp): - raise ValueError( - "The provided timestamp '{}' is invalid.".format(timestamp) - ) - - timestamp = util.normalize_timestamp(float(timestamp)) - dt = datetime.utcfromtimestamp(timestamp) - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dateutil_tz.tzutc(), - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def fromdatetime(cls, dt, tzinfo=None): - """Constructs an :class:`Arrow ` object from a ``datetime`` and - optional replacement timezone. - - :param dt: the ``datetime`` - :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to ``dt``'s - timezone, or UTC if naive. - - If you only want to replace the timezone of naive datetimes:: - - >>> dt - datetime.datetime(2013, 5, 5, 0, 0, tzinfo=tzutc()) - >>> arrow.Arrow.fromdatetime(dt, dt.tzinfo or 'US/Pacific') - - - """ - - if tzinfo is None: - if dt.tzinfo is None: - tzinfo = dateutil_tz.tzutc() - else: - tzinfo = dt.tzinfo - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo, - fold=getattr(dt, "fold", 0), - ) - - @classmethod - def fromdate(cls, date, tzinfo=None): - """Constructs an :class:`Arrow ` object from a ``date`` and optional - replacement timezone. Time values are set to 0. - - :param date: the ``date`` - :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC. - """ - - if tzinfo is None: - tzinfo = dateutil_tz.tzutc() - - return cls(date.year, date.month, date.day, tzinfo=tzinfo) - - @classmethod - def strptime(cls, date_str, fmt, tzinfo=None): - """Constructs an :class:`Arrow ` object from a date string and format, - in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. - - :param date_str: the date string. - :param fmt: the format string. - :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to the parsed - timezone if ``fmt`` contains a timezone directive, otherwise UTC. - - Usage:: - - >>> arrow.Arrow.strptime('20-01-2019 15:49:10', '%d-%m-%Y %H:%M:%S') - - - """ - - dt = datetime.strptime(date_str, fmt) - if tzinfo is None: - tzinfo = dt.tzinfo - - return cls( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - tzinfo, - fold=getattr(dt, "fold", 0), - ) - - # factories: ranges and spans - - @classmethod - def range(cls, frame, start, end=None, tz=None, limit=None): - """Returns an iterator of :class:`Arrow ` objects, representing - points in time between two inputs. - - :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param start: A datetime expression, the start of the range. - :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A :ref:`timezone expression `. Defaults to - ``start``'s timezone, or UTC if ``start`` is naive. - :param limit: (optional) A maximum number of tuples to return. - - **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to - return the entire range. Call with ``limit`` alone to return a maximum # of results from - the start. Call with both to cap a range at a maximum # of results. - - **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before - iterating. As such, either call with naive objects and ``tz``, or aware objects from the - same timezone and no ``tz``. - - Supported frame values: year, quarter, month, week, day, hour, minute, second. - - Recognized datetime expressions: - - - An :class:`Arrow ` object. - - A ``datetime`` object. - - Usage:: - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.range('hour', start, end): - ... print(repr(r)) - ... - - - - - - - **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned iterator:: - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 13, 30) - >>> for r in arrow.Arrow.range('hour', start, end): - ... print(repr(r)) - ... - - - - """ - - _, frame_relative, relative_steps = cls._get_frames(frame) - - tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) - - start = cls._get_datetime(start).replace(tzinfo=tzinfo) - end, limit = cls._get_iteration_params(end, limit) - end = cls._get_datetime(end).replace(tzinfo=tzinfo) - - current = cls.fromdatetime(start) - original_day = start.day - day_is_clipped = False - i = 0 - - while current <= end and i < limit: - i += 1 - yield current - - values = [getattr(current, f) for f in cls._ATTRS] - current = cls(*values, tzinfo=tzinfo).shift( - **{frame_relative: relative_steps} - ) - - if frame in ["month", "quarter", "year"] and current.day < original_day: - day_is_clipped = True - - if day_is_clipped and not cls._is_last_day_of_month(current): - current = current.replace(day=original_day) - - def span(self, frame, count=1, bounds="[)"): - """Returns two new :class:`Arrow ` objects, representing the timespan - of the :class:`Arrow ` object in a given timeframe. - - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param count: (optional) the number of frames to span. - :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in the span. '(' excludes - the start, '[' includes the start, ')' excludes the end, and ']' includes the end. - If the bounds are not specified, the default bound '[)' is used. - - Supported frame values: year, quarter, month, week, day, hour, minute, second. - - Usage:: - - >>> arrow.utcnow() - - - >>> arrow.utcnow().span('hour') - (, ) - - >>> arrow.utcnow().span('day') - (, ) - - >>> arrow.utcnow().span('day', count=2) - (, ) - - >>> arrow.utcnow().span('day', bounds='[]') - (, ) - - """ - - util.validate_bounds(bounds) - - frame_absolute, frame_relative, relative_steps = self._get_frames(frame) - - if frame_absolute == "week": - attr = "day" - elif frame_absolute == "quarter": - attr = "month" - else: - attr = frame_absolute - - index = self._ATTRS.index(attr) - frames = self._ATTRS[: index + 1] - - values = [getattr(self, f) for f in frames] - - for _ in range(3 - len(values)): - values.append(1) - - floor = self.__class__(*values, tzinfo=self.tzinfo) - - if frame_absolute == "week": - floor = floor.shift(days=-(self.isoweekday() - 1)) - elif frame_absolute == "quarter": - floor = floor.shift(months=-((self.month - 1) % 3)) - - ceil = floor.shift(**{frame_relative: count * relative_steps}) - - if bounds[0] == "(": - floor = floor.shift(microseconds=+1) - - if bounds[1] == ")": - ceil = ceil.shift(microseconds=-1) - - return floor, ceil - - def floor(self, frame): - """Returns a new :class:`Arrow ` object, representing the "floor" - of the timespan of the :class:`Arrow ` object in a given timeframe. - Equivalent to the first element in the 2-tuple returned by - :func:`span `. - - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - - Usage:: - - >>> arrow.utcnow().floor('hour') - - """ - - return self.span(frame)[0] - - def ceil(self, frame): - """Returns a new :class:`Arrow ` object, representing the "ceiling" - of the timespan of the :class:`Arrow ` object in a given timeframe. - Equivalent to the second element in the 2-tuple returned by - :func:`span `. - - :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). - - Usage:: - - >>> arrow.utcnow().ceil('hour') - - """ - - return self.span(frame)[1] - - @classmethod - def span_range(cls, frame, start, end, tz=None, limit=None, bounds="[)"): - """Returns an iterator of tuples, each :class:`Arrow ` objects, - representing a series of timespans between two inputs. - - :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param start: A datetime expression, the start of the range. - :param end: (optional) A datetime expression, the end of the range. - :param tz: (optional) A :ref:`timezone expression `. Defaults to - ``start``'s timezone, or UTC if ``start`` is naive. - :param limit: (optional) A maximum number of tuples to return. - :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in each span in the range. '(' excludes - the start, '[' includes the start, ')' excludes the end, and ']' includes the end. - If the bounds are not specified, the default bound '[)' is used. - - **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to - return the entire range. Call with ``limit`` alone to return a maximum # of results from - the start. Call with both to cap a range at a maximum # of results. - - **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before - iterating. As such, either call with naive objects and ``tz``, or aware objects from the - same timezone and no ``tz``. - - Supported frame values: year, quarter, month, week, day, hour, minute, second. - - Recognized datetime expressions: - - - An :class:`Arrow ` object. - - A ``datetime`` object. - - **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned - iterator of timespans. - - Usage: - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.span_range('hour', start, end): - ... print(r) - ... - (, ) - (, ) - (, ) - (, ) - (, ) - (, ) - - """ - - tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) - start = cls.fromdatetime(start, tzinfo).span(frame)[0] - _range = cls.range(frame, start, end, tz, limit) - return (r.span(frame, bounds=bounds) for r in _range) - - @classmethod - def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"): - """Returns an iterator of tuples, each :class:`Arrow ` objects, - representing a series of intervals between two inputs. - - :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). - :param start: A datetime expression, the start of the range. - :param end: (optional) A datetime expression, the end of the range. - :param interval: (optional) Time interval for the given time frame. - :param tz: (optional) A timezone expression. Defaults to UTC. - :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in the intervals. '(' excludes - the start, '[' includes the start, ')' excludes the end, and ']' includes the end. - If the bounds are not specified, the default bound '[)' is used. - - Supported frame values: year, quarter, month, week, day, hour, minute, second - - Recognized datetime expressions: - - - An :class:`Arrow ` object. - - A ``datetime`` object. - - Recognized timezone expressions: - - - A ``tzinfo`` object. - - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. - - A ``str`` in ISO 8601 style, as in '+07:00'. - - A ``str``, one of the following: 'local', 'utc', 'UTC'. - - Usage: - - >>> start = datetime(2013, 5, 5, 12, 30) - >>> end = datetime(2013, 5, 5, 17, 15) - >>> for r in arrow.Arrow.interval('hour', start, end, 2): - ... print r - ... - (, ) - (, ) - (, ) - """ - if interval < 1: - raise ValueError("interval has to be a positive integer") - - spanRange = iter(cls.span_range(frame, start, end, tz, bounds=bounds)) - while True: - try: - intvlStart, intvlEnd = next(spanRange) - for _ in range(interval - 1): - _, intvlEnd = next(spanRange) - yield intvlStart, intvlEnd - except StopIteration: - return - - # representations - - def __repr__(self): - return "<{} [{}]>".format(self.__class__.__name__, self.__str__()) - - def __str__(self): - return self._datetime.isoformat() - - def __format__(self, formatstr): - - if len(formatstr) > 0: - return self.format(formatstr) - - return str(self) - - def __hash__(self): - return self._datetime.__hash__() - - # attributes and properties - - def __getattr__(self, name): - - if name == "week": - return self.isocalendar()[1] - - if name == "quarter": - return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1 - - if not name.startswith("_"): - value = getattr(self._datetime, name, None) - - if value is not None: - return value - - return object.__getattribute__(self, name) - - @property - def tzinfo(self): - """Gets the ``tzinfo`` of the :class:`Arrow ` object. - - Usage:: - - >>> arw=arrow.utcnow() - >>> arw.tzinfo - tzutc() - - """ - - return self._datetime.tzinfo - - @tzinfo.setter - def tzinfo(self, tzinfo): - """ Sets the ``tzinfo`` of the :class:`Arrow ` object. """ - - self._datetime = self._datetime.replace(tzinfo=tzinfo) - - @property - def datetime(self): - """Returns a datetime representation of the :class:`Arrow ` object. - - Usage:: - - >>> arw=arrow.utcnow() - >>> arw.datetime - datetime.datetime(2019, 1, 24, 16, 35, 27, 276649, tzinfo=tzutc()) - - """ - - return self._datetime - - @property - def naive(self): - """Returns a naive datetime representation of the :class:`Arrow ` - object. - - Usage:: - - >>> nairobi = arrow.now('Africa/Nairobi') - >>> nairobi - - >>> nairobi.naive - datetime.datetime(2019, 1, 23, 19, 27, 12, 297999) - - """ - - return self._datetime.replace(tzinfo=None) - - @property - def timestamp(self): - """Returns a timestamp representation of the :class:`Arrow ` object, in - UTC time. - - Usage:: - - >>> arrow.utcnow().timestamp - 1548260567 - - """ - - warnings.warn( - "For compatibility with the datetime.timestamp() method this property will be replaced with a method in " - "the 1.0.0 release, please switch to the .int_timestamp property for identical behaviour as soon as " - "possible.", - DeprecationWarning, - ) - return calendar.timegm(self._datetime.utctimetuple()) - - @property - def int_timestamp(self): - """Returns a timestamp representation of the :class:`Arrow ` object, in - UTC time. - - Usage:: - - >>> arrow.utcnow().int_timestamp - 1548260567 - - """ - - return calendar.timegm(self._datetime.utctimetuple()) - - @property - def float_timestamp(self): - """Returns a floating-point representation of the :class:`Arrow ` - object, in UTC time. - - Usage:: - - >>> arrow.utcnow().float_timestamp - 1548260516.830896 - - """ - - # IDEA get rid of this in 1.0.0 and wrap datetime.timestamp() - # Or for compatibility retain this but make it call the timestamp method - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - return self.timestamp + float(self.microsecond) / 1000000 - - @property - def fold(self): - """ Returns the ``fold`` value of the :class:`Arrow ` object. """ - - # in python < 3.6 _datetime will be a _DatetimeWithFold if fold=1 and a datetime with no fold attribute - # otherwise, so we need to return zero to cover the latter case - return getattr(self._datetime, "fold", 0) - - @property - def ambiguous(self): - """ Returns a boolean indicating whether the :class:`Arrow ` object is ambiguous.""" - - return dateutil_tz.datetime_ambiguous(self._datetime) - - @property - def imaginary(self): - """Indicates whether the :class: `Arrow ` object exists in the current timezone.""" - - return not dateutil_tz.datetime_exists(self._datetime) - - # mutation and duplication. - - def clone(self): - """Returns a new :class:`Arrow ` object, cloned from the current one. - - Usage: - - >>> arw = arrow.utcnow() - >>> cloned = arw.clone() - - """ - - return self.fromdatetime(self._datetime) - - def replace(self, **kwargs): - """Returns a new :class:`Arrow ` object with attributes updated - according to inputs. - - Use property names to set their value absolutely:: - - >>> import arrow - >>> arw = arrow.utcnow() - >>> arw - - >>> arw.replace(year=2014, month=6) - - - You can also replace the timezone without conversion, using a - :ref:`timezone expression `:: - - >>> arw.replace(tzinfo=tz.tzlocal()) - - - """ - - absolute_kwargs = {} - - for key, value in kwargs.items(): - - if key in self._ATTRS: - absolute_kwargs[key] = value - elif key in ["week", "quarter"]: - raise AttributeError("setting absolute {} is not supported".format(key)) - elif key not in ["tzinfo", "fold"]: - raise AttributeError('unknown attribute: "{}"'.format(key)) - - current = self._datetime.replace(**absolute_kwargs) - - tzinfo = kwargs.get("tzinfo") - - if tzinfo is not None: - tzinfo = self._get_tzinfo(tzinfo) - current = current.replace(tzinfo=tzinfo) - - fold = kwargs.get("fold") - - # TODO revisit this once we drop support for 2.7/3.5 - if fold is not None: - current = dateutil_tz.enfold(current, fold=fold) - - return self.fromdatetime(current) - - def shift(self, **kwargs): - """Returns a new :class:`Arrow ` object with attributes updated - according to inputs. - - Use pluralized property names to relatively shift their current value: - - >>> import arrow - >>> arw = arrow.utcnow() - >>> arw - - >>> arw.shift(years=1, months=-1) - - - Day-of-the-week relative shifting can use either Python's weekday numbers - (Monday = 0, Tuesday = 1 .. Sunday = 6) or using dateutil.relativedelta's - day instances (MO, TU .. SU). When using weekday numbers, the returned - date will always be greater than or equal to the starting date. - - Using the above code (which is a Saturday) and asking it to shift to Saturday: - - >>> arw.shift(weekday=5) - - - While asking for a Monday: - - >>> arw.shift(weekday=0) - - - """ - - relative_kwargs = {} - additional_attrs = ["weeks", "quarters", "weekday"] - - for key, value in kwargs.items(): - - if key in self._ATTRS_PLURAL or key in additional_attrs: - relative_kwargs[key] = value - else: - raise AttributeError( - "Invalid shift time frame. Please select one of the following: {}.".format( - ", ".join(self._ATTRS_PLURAL + additional_attrs) - ) - ) - - # core datetime does not support quarters, translate to months. - relative_kwargs.setdefault("months", 0) - relative_kwargs["months"] += ( - relative_kwargs.pop("quarters", 0) * self._MONTHS_PER_QUARTER - ) - - current = self._datetime + relativedelta(**relative_kwargs) - - if not dateutil_tz.datetime_exists(current): - current = dateutil_tz.resolve_imaginary(current) - - return self.fromdatetime(current) - - def to(self, tz): - """Returns a new :class:`Arrow ` object, converted - to the target timezone. - - :param tz: A :ref:`timezone expression `. - - Usage:: - - >>> utc = arrow.utcnow() - >>> utc - - - >>> utc.to('US/Pacific') - - - >>> utc.to(tz.tzlocal()) - - - >>> utc.to('-07:00') - - - >>> utc.to('local') - - - >>> utc.to('local').to('utc') - - - """ - - if not isinstance(tz, dt_tzinfo): - tz = parser.TzinfoParser.parse(tz) - - dt = self._datetime.astimezone(tz) - - return self.__class__( - dt.year, - dt.month, - dt.day, - dt.hour, - dt.minute, - dt.second, - dt.microsecond, - dt.tzinfo, - fold=getattr(dt, "fold", 0), - ) - - # string output and formatting - - def format(self, fmt="YYYY-MM-DD HH:mm:ssZZ", locale="en_us"): - """Returns a string representation of the :class:`Arrow ` object, - formatted according to a format string. - - :param fmt: the format string. - - Usage:: - - >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') - '2013-05-09 03:56:47 -00:00' - - >>> arrow.utcnow().format('X') - '1368071882' - - >>> arrow.utcnow().format('MMMM DD, YYYY') - 'May 09, 2013' - - >>> arrow.utcnow().format() - '2013-05-09 03:56:47 -00:00' - - """ - - return formatter.DateTimeFormatter(locale).format(self._datetime, fmt) - - def humanize( - self, other=None, locale="en_us", only_distance=False, granularity="auto" - ): - """Returns a localized, humanized representation of a relative difference in time. - - :param other: (optional) an :class:`Arrow ` or ``datetime`` object. - Defaults to now in the current :class:`Arrow ` object's timezone. - :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'. - :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. - :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', - 'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings - - Usage:: - - >>> earlier = arrow.utcnow().shift(hours=-2) - >>> earlier.humanize() - '2 hours ago' - - >>> later = earlier.shift(hours=4) - >>> later.humanize(earlier) - 'in 4 hours' - - """ - - locale_name = locale - locale = locales.get_locale(locale) - - if other is None: - utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) - dt = utc.astimezone(self._datetime.tzinfo) - - elif isinstance(other, Arrow): - dt = other._datetime - - elif isinstance(other, datetime): - if other.tzinfo is None: - dt = other.replace(tzinfo=self._datetime.tzinfo) - else: - dt = other.astimezone(self._datetime.tzinfo) - - else: - raise TypeError( - "Invalid 'other' argument of type '{}'. " - "Argument must be of type None, Arrow, or datetime.".format( - type(other).__name__ - ) - ) - - if isinstance(granularity, list) and len(granularity) == 1: - granularity = granularity[0] - - delta = int(round(util.total_seconds(self._datetime - dt))) - sign = -1 if delta < 0 else 1 - diff = abs(delta) - delta = diff - - try: - if granularity == "auto": - if diff < 10: - return locale.describe("now", only_distance=only_distance) - - if diff < 45: - seconds = sign * delta - return locale.describe( - "seconds", seconds, only_distance=only_distance - ) - - elif diff < 90: - return locale.describe("minute", sign, only_distance=only_distance) - elif diff < 2700: - minutes = sign * int(max(delta / 60, 2)) - return locale.describe( - "minutes", minutes, only_distance=only_distance - ) - - elif diff < 5400: - return locale.describe("hour", sign, only_distance=only_distance) - elif diff < 79200: - hours = sign * int(max(delta / 3600, 2)) - return locale.describe("hours", hours, only_distance=only_distance) - - # anything less than 48 hours should be 1 day - elif diff < 172800: - return locale.describe("day", sign, only_distance=only_distance) - elif diff < 554400: - days = sign * int(max(delta / 86400, 2)) - return locale.describe("days", days, only_distance=only_distance) - - elif diff < 907200: - return locale.describe("week", sign, only_distance=only_distance) - elif diff < 2419200: - weeks = sign * int(max(delta / 604800, 2)) - return locale.describe("weeks", weeks, only_distance=only_distance) - - elif diff < 3888000: - return locale.describe("month", sign, only_distance=only_distance) - elif diff < 29808000: - self_months = self._datetime.year * 12 + self._datetime.month - other_months = dt.year * 12 + dt.month - - months = sign * int(max(abs(other_months - self_months), 2)) - - return locale.describe( - "months", months, only_distance=only_distance - ) - - elif diff < 47260800: - return locale.describe("year", sign, only_distance=only_distance) - else: - years = sign * int(max(delta / 31536000, 2)) - return locale.describe("years", years, only_distance=only_distance) - - elif util.isstr(granularity): - if granularity == "second": - delta = sign * delta - if abs(delta) < 2: - return locale.describe("now", only_distance=only_distance) - elif granularity == "minute": - delta = sign * delta / self._SECS_PER_MINUTE - elif granularity == "hour": - delta = sign * delta / self._SECS_PER_HOUR - elif granularity == "day": - delta = sign * delta / self._SECS_PER_DAY - elif granularity == "week": - delta = sign * delta / self._SECS_PER_WEEK - elif granularity == "month": - delta = sign * delta / self._SECS_PER_MONTH - elif granularity == "year": - delta = sign * delta / self._SECS_PER_YEAR - else: - raise AttributeError( - "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'" - ) - - if trunc(abs(delta)) != 1: - granularity += "s" - return locale.describe(granularity, delta, only_distance=only_distance) - - else: - timeframes = [] - if "year" in granularity: - years = sign * delta / self._SECS_PER_YEAR - delta %= self._SECS_PER_YEAR - timeframes.append(["year", years]) - - if "month" in granularity: - months = sign * delta / self._SECS_PER_MONTH - delta %= self._SECS_PER_MONTH - timeframes.append(["month", months]) - - if "week" in granularity: - weeks = sign * delta / self._SECS_PER_WEEK - delta %= self._SECS_PER_WEEK - timeframes.append(["week", weeks]) - - if "day" in granularity: - days = sign * delta / self._SECS_PER_DAY - delta %= self._SECS_PER_DAY - timeframes.append(["day", days]) - - if "hour" in granularity: - hours = sign * delta / self._SECS_PER_HOUR - delta %= self._SECS_PER_HOUR - timeframes.append(["hour", hours]) - - if "minute" in granularity: - minutes = sign * delta / self._SECS_PER_MINUTE - delta %= self._SECS_PER_MINUTE - timeframes.append(["minute", minutes]) - - if "second" in granularity: - seconds = sign * delta - timeframes.append(["second", seconds]) - - if len(timeframes) < len(granularity): - raise AttributeError( - "Invalid level of granularity. " - "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'." - ) - - for tf in timeframes: - # Make granularity plural if the delta is not equal to 1 - if trunc(abs(tf[1])) != 1: - tf[0] += "s" - return locale.describe_multi(timeframes, only_distance=only_distance) - - except KeyError as e: - raise ValueError( - "Humanization of the {} granularity is not currently translated in the '{}' locale. " - "Please consider making a contribution to this locale.".format( - e, locale_name - ) - ) - - # query functions - - def is_between(self, start, end, bounds="()"): - """Returns a boolean denoting whether the specified date and time is between - the start and end dates and times. - - :param start: an :class:`Arrow ` object. - :param end: an :class:`Arrow ` object. - :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies - whether to include or exclude the start and end values in the range. '(' excludes - the start, '[' includes the start, ')' excludes the end, and ']' includes the end. - If the bounds are not specified, the default bound '()' is used. - - Usage:: - - >>> start = arrow.get(datetime(2013, 5, 5, 12, 30, 10)) - >>> end = arrow.get(datetime(2013, 5, 5, 12, 30, 36)) - >>> arrow.get(datetime(2013, 5, 5, 12, 30, 27)).is_between(start, end) - True - - >>> start = arrow.get(datetime(2013, 5, 5)) - >>> end = arrow.get(datetime(2013, 5, 8)) - >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[]') - True - - >>> start = arrow.get(datetime(2013, 5, 5)) - >>> end = arrow.get(datetime(2013, 5, 8)) - >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)') - False - - """ - - util.validate_bounds(bounds) - - if not isinstance(start, Arrow): - raise TypeError( - "Can't parse start date argument type of '{}'".format(type(start)) - ) - - if not isinstance(end, Arrow): - raise TypeError( - "Can't parse end date argument type of '{}'".format(type(end)) - ) - - include_start = bounds[0] == "[" - include_end = bounds[1] == "]" - - target_timestamp = self.float_timestamp - start_timestamp = start.float_timestamp - end_timestamp = end.float_timestamp - - if include_start and include_end: - return ( - target_timestamp >= start_timestamp - and target_timestamp <= end_timestamp - ) - elif include_start and not include_end: - return ( - target_timestamp >= start_timestamp and target_timestamp < end_timestamp - ) - elif not include_start and include_end: - return ( - target_timestamp > start_timestamp and target_timestamp <= end_timestamp - ) - else: - return ( - target_timestamp > start_timestamp and target_timestamp < end_timestamp - ) - - # datetime methods - - def date(self): - """Returns a ``date`` object with the same year, month and day. - - Usage:: - - >>> arrow.utcnow().date() - datetime.date(2019, 1, 23) - - """ - - return self._datetime.date() - - def time(self): - """Returns a ``time`` object with the same hour, minute, second, microsecond. - - Usage:: - - >>> arrow.utcnow().time() - datetime.time(12, 15, 34, 68352) - - """ - - return self._datetime.time() - - def timetz(self): - """Returns a ``time`` object with the same hour, minute, second, microsecond and - tzinfo. - - Usage:: - - >>> arrow.utcnow().timetz() - datetime.time(12, 5, 18, 298893, tzinfo=tzutc()) - - """ - - return self._datetime.timetz() - - def astimezone(self, tz): - """Returns a ``datetime`` object, converted to the specified timezone. - - :param tz: a ``tzinfo`` object. - - Usage:: - - >>> pacific=arrow.now('US/Pacific') - >>> nyc=arrow.now('America/New_York').tzinfo - >>> pacific.astimezone(nyc) - datetime.datetime(2019, 1, 20, 10, 24, 22, 328172, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York')) - - """ - - return self._datetime.astimezone(tz) - - def utcoffset(self): - """Returns a ``timedelta`` object representing the whole number of minutes difference from - UTC time. - - Usage:: - - >>> arrow.now('US/Pacific').utcoffset() - datetime.timedelta(-1, 57600) - - """ - - return self._datetime.utcoffset() - - def dst(self): - """Returns the daylight savings time adjustment. - - Usage:: - - >>> arrow.utcnow().dst() - datetime.timedelta(0) - - """ - - return self._datetime.dst() - - def timetuple(self): - """Returns a ``time.struct_time``, in the current timezone. - - Usage:: - - >>> arrow.utcnow().timetuple() - time.struct_time(tm_year=2019, tm_mon=1, tm_mday=20, tm_hour=15, tm_min=17, tm_sec=8, tm_wday=6, tm_yday=20, tm_isdst=0) - - """ - - return self._datetime.timetuple() - - def utctimetuple(self): - """Returns a ``time.struct_time``, in UTC time. - - Usage:: - - >>> arrow.utcnow().utctimetuple() - time.struct_time(tm_year=2019, tm_mon=1, tm_mday=19, tm_hour=21, tm_min=41, tm_sec=7, tm_wday=5, tm_yday=19, tm_isdst=0) - - """ - - return self._datetime.utctimetuple() - - def toordinal(self): - """Returns the proleptic Gregorian ordinal of the date. - - Usage:: - - >>> arrow.utcnow().toordinal() - 737078 - - """ - - return self._datetime.toordinal() - - def weekday(self): - """Returns the day of the week as an integer (0-6). - - Usage:: - - >>> arrow.utcnow().weekday() - 5 - - """ - - return self._datetime.weekday() - - def isoweekday(self): - """Returns the ISO day of the week as an integer (1-7). - - Usage:: - - >>> arrow.utcnow().isoweekday() - 6 - - """ - - return self._datetime.isoweekday() - - def isocalendar(self): - """Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). - - Usage:: - - >>> arrow.utcnow().isocalendar() - (2019, 3, 6) - - """ - - return self._datetime.isocalendar() - - def isoformat(self, sep="T"): - """Returns an ISO 8601 formatted representation of the date and time. - - Usage:: - - >>> arrow.utcnow().isoformat() - '2019-01-19T18:30:52.442118+00:00' - - """ - - return self._datetime.isoformat(sep) - - def ctime(self): - """Returns a ctime formatted representation of the date and time. - - Usage:: - - >>> arrow.utcnow().ctime() - 'Sat Jan 19 18:26:50 2019' - - """ - - return self._datetime.ctime() - - def strftime(self, format): - """Formats in the style of ``datetime.strftime``. - - :param format: the format string. - - Usage:: - - >>> arrow.utcnow().strftime('%d-%m-%Y %H:%M:%S') - '23-01-2019 12:28:17' - - """ - - return self._datetime.strftime(format) - - def for_json(self): - """Serializes for the ``for_json`` protocol of simplejson. - - Usage:: - - >>> arrow.utcnow().for_json() - '2019-01-19T18:25:36.760079+00:00' - - """ - - return self.isoformat() - - # math - - def __add__(self, other): - - if isinstance(other, (timedelta, relativedelta)): - return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) - - return NotImplemented - - def __radd__(self, other): - return self.__add__(other) - - def __sub__(self, other): - - if isinstance(other, (timedelta, relativedelta)): - return self.fromdatetime(self._datetime - other, self._datetime.tzinfo) - - elif isinstance(other, datetime): - return self._datetime - other - - elif isinstance(other, Arrow): - return self._datetime - other._datetime - - return NotImplemented - - def __rsub__(self, other): - - if isinstance(other, datetime): - return other - self._datetime - - return NotImplemented - - # comparisons - - def __eq__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return False - - return self._datetime == self._get_datetime(other) - - def __ne__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return True - - return not self.__eq__(other) - - def __gt__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return NotImplemented - - return self._datetime > self._get_datetime(other) - - def __ge__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return NotImplemented - - return self._datetime >= self._get_datetime(other) - - def __lt__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return NotImplemented - - return self._datetime < self._get_datetime(other) - - def __le__(self, other): - - if not isinstance(other, (Arrow, datetime)): - return NotImplemented - - return self._datetime <= self._get_datetime(other) - - def __cmp__(self, other): - if sys.version_info[0] < 3: # pragma: no cover - if not isinstance(other, (Arrow, datetime)): - raise TypeError( - "can't compare '{}' to '{}'".format(type(self), type(other)) - ) - - # internal methods - - @staticmethod - def _get_tzinfo(tz_expr): - - if tz_expr is None: - return dateutil_tz.tzutc() - if isinstance(tz_expr, dt_tzinfo): - return tz_expr - else: - try: - return parser.TzinfoParser.parse(tz_expr) - except parser.ParserError: - raise ValueError("'{}' not recognized as a timezone".format(tz_expr)) - - @classmethod - def _get_datetime(cls, expr): - """Get datetime object for a specified expression.""" - if isinstance(expr, Arrow): - return expr.datetime - elif isinstance(expr, datetime): - return expr - elif util.is_timestamp(expr): - timestamp = float(expr) - return cls.utcfromtimestamp(timestamp).datetime - else: - raise ValueError( - "'{}' not recognized as a datetime or timestamp.".format(expr) - ) - - @classmethod - def _get_frames(cls, name): - - if name in cls._ATTRS: - return name, "{}s".format(name), 1 - elif name[-1] == "s" and name[:-1] in cls._ATTRS: - return name[:-1], name, 1 - elif name in ["week", "weeks"]: - return "week", "weeks", 1 - elif name in ["quarter", "quarters"]: - return "quarter", "months", 3 - - supported = ", ".join( - [ - "year(s)", - "month(s)", - "day(s)", - "hour(s)", - "minute(s)", - "second(s)", - "microsecond(s)", - "week(s)", - "quarter(s)", - ] - ) - raise AttributeError( - "range/span over frame {} not supported. Supported frames: {}".format( - name, supported - ) - ) - - @classmethod - def _get_iteration_params(cls, end, limit): - - if end is None: - - if limit is None: - raise ValueError("one of 'end' or 'limit' is required") - - return cls.max, limit - - else: - if limit is None: - return end, sys.maxsize - return end, limit - - @staticmethod - def _is_last_day_of_month(date): - return date.day == calendar.monthrange(date.year, date.month)[1] - - -Arrow.min = Arrow.fromdatetime(datetime.min) -Arrow.max = Arrow.fromdatetime(datetime.max) diff --git a/client/ayon_core/vendor/python/python_2/arrow/constants.py b/client/ayon_core/vendor/python/python_2/arrow/constants.py deleted file mode 100644 index 81e37b26de..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/constants.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- - -# Output of time.mktime(datetime.max.timetuple()) on macOS -# This value must be hardcoded for compatibility with Windows -# Platform-independent max timestamps are hard to form -# https://stackoverflow.com/q/46133223 -MAX_TIMESTAMP = 253402318799.0 -MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000 -MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000 diff --git a/client/ayon_core/vendor/python/python_2/arrow/factory.py b/client/ayon_core/vendor/python/python_2/arrow/factory.py deleted file mode 100644 index 05933e8151..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/factory.py +++ /dev/null @@ -1,301 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Implements the :class:`ArrowFactory ` class, -providing factory methods for common :class:`Arrow ` -construction scenarios. - -""" - -from __future__ import absolute_import - -import calendar -from datetime import date, datetime -from datetime import tzinfo as dt_tzinfo -from time import struct_time - -from dateutil import tz as dateutil_tz - -from arrow import parser -from arrow.arrow import Arrow -from arrow.util import is_timestamp, iso_to_gregorian, isstr - - -class ArrowFactory(object): - """A factory for generating :class:`Arrow ` objects. - - :param type: (optional) the :class:`Arrow `-based class to construct from. - Defaults to :class:`Arrow `. - - """ - - def __init__(self, type=Arrow): - self.type = type - - def get(self, *args, **kwargs): - """Returns an :class:`Arrow ` object based on flexible inputs. - - :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en_us'. - :param tzinfo: (optional) a :ref:`timezone expression ` or tzinfo object. - Replaces the timezone unless using an input form that is explicitly UTC or specifies - the timezone in a positional argument. Defaults to UTC. - :param normalize_whitespace: (optional) a ``bool`` specifying whether or not to normalize - redundant whitespace (spaces, tabs, and newlines) in a datetime string before parsing. - Defaults to false. - - Usage:: - - >>> import arrow - - **No inputs** to get current UTC time:: - - >>> arrow.get() - - - **None** to also get current UTC time:: - - >>> arrow.get(None) - - - **One** :class:`Arrow ` object, to get a copy. - - >>> arw = arrow.utcnow() - >>> arrow.get(arw) - - - **One** ``float`` or ``int``, convertible to a floating-point timestamp, to get - that timestamp in UTC:: - - >>> arrow.get(1367992474.293378) - - - >>> arrow.get(1367992474) - - - **One** ISO 8601-formatted ``str``, to parse it:: - - >>> arrow.get('2013-09-29T01:26:43.830580') - - - **One** ISO 8601-formatted ``str``, in basic format, to parse it:: - - >>> arrow.get('20160413T133656.456289') - - - **One** ``tzinfo``, to get the current time **converted** to that timezone:: - - >>> arrow.get(tz.tzlocal()) - - - **One** naive ``datetime``, to get that datetime in UTC:: - - >>> arrow.get(datetime(2013, 5, 5)) - - - **One** aware ``datetime``, to get that datetime:: - - >>> arrow.get(datetime(2013, 5, 5, tzinfo=tz.tzlocal())) - - - **One** naive ``date``, to get that date in UTC:: - - >>> arrow.get(date(2013, 5, 5)) - - - **One** time.struct time:: - - >>> arrow.get(gmtime(0)) - - - **One** iso calendar ``tuple``, to get that week date in UTC:: - - >>> arrow.get((2013, 18, 7)) - - - **Two** arguments, a naive or aware ``datetime``, and a replacement - :ref:`timezone expression `:: - - >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific') - - - **Two** arguments, a naive ``date``, and a replacement - :ref:`timezone expression `:: - - >>> arrow.get(date(2013, 5, 5), 'US/Pacific') - - - **Two** arguments, both ``str``, to parse the first according to the format of the second:: - - >>> arrow.get('2013-05-05 12:30:45 America/Chicago', 'YYYY-MM-DD HH:mm:ss ZZZ') - - - **Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try:: - - >>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss']) - - - **Three or more** arguments, as for the constructor of a ``datetime``:: - - >>> arrow.get(2013, 5, 5, 12, 30, 45) - - - """ - - arg_count = len(args) - locale = kwargs.pop("locale", "en_us") - tz = kwargs.get("tzinfo", None) - normalize_whitespace = kwargs.pop("normalize_whitespace", False) - - # if kwargs given, send to constructor unless only tzinfo provided - if len(kwargs) > 1: - arg_count = 3 - - # tzinfo kwarg is not provided - if len(kwargs) == 1 and tz is None: - arg_count = 3 - - # () -> now, @ utc. - if arg_count == 0: - if isstr(tz): - tz = parser.TzinfoParser.parse(tz) - return self.type.now(tz) - - if isinstance(tz, dt_tzinfo): - return self.type.now(tz) - - return self.type.utcnow() - - if arg_count == 1: - arg = args[0] - - # (None) -> now, @ utc. - if arg is None: - return self.type.utcnow() - - # try (int, float) -> from timestamp with tz - elif not isstr(arg) and is_timestamp(arg): - if tz is None: - # set to UTC by default - tz = dateutil_tz.tzutc() - return self.type.fromtimestamp(arg, tzinfo=tz) - - # (Arrow) -> from the object's datetime. - elif isinstance(arg, Arrow): - return self.type.fromdatetime(arg.datetime) - - # (datetime) -> from datetime. - elif isinstance(arg, datetime): - return self.type.fromdatetime(arg) - - # (date) -> from date. - elif isinstance(arg, date): - return self.type.fromdate(arg) - - # (tzinfo) -> now, @ tzinfo. - elif isinstance(arg, dt_tzinfo): - return self.type.now(arg) - - # (str) -> parse. - elif isstr(arg): - dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace) - return self.type.fromdatetime(dt, tz) - - # (struct_time) -> from struct_time - elif isinstance(arg, struct_time): - return self.type.utcfromtimestamp(calendar.timegm(arg)) - - # (iso calendar) -> convert then from date - elif isinstance(arg, tuple) and len(arg) == 3: - dt = iso_to_gregorian(*arg) - return self.type.fromdate(dt) - - else: - raise TypeError( - "Can't parse single argument of type '{}'".format(type(arg)) - ) - - elif arg_count == 2: - - arg_1, arg_2 = args[0], args[1] - - if isinstance(arg_1, datetime): - - # (datetime, tzinfo/str) -> fromdatetime replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): - return self.type.fromdatetime(arg_1, arg_2) - else: - raise TypeError( - "Can't parse two arguments of types 'datetime', '{}'".format( - type(arg_2) - ) - ) - - elif isinstance(arg_1, date): - - # (date, tzinfo/str) -> fromdate replace tzinfo. - if isinstance(arg_2, dt_tzinfo) or isstr(arg_2): - return self.type.fromdate(arg_1, tzinfo=arg_2) - else: - raise TypeError( - "Can't parse two arguments of types 'date', '{}'".format( - type(arg_2) - ) - ) - - # (str, format) -> parse. - elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)): - dt = parser.DateTimeParser(locale).parse( - args[0], args[1], normalize_whitespace - ) - return self.type.fromdatetime(dt, tzinfo=tz) - - else: - raise TypeError( - "Can't parse two arguments of types '{}' and '{}'".format( - type(arg_1), type(arg_2) - ) - ) - - # 3+ args -> datetime-like via constructor. - else: - return self.type(*args, **kwargs) - - def utcnow(self): - """Returns an :class:`Arrow ` object, representing "now" in UTC time. - - Usage:: - - >>> import arrow - >>> arrow.utcnow() - - """ - - return self.type.utcnow() - - def now(self, tz=None): - """Returns an :class:`Arrow ` object, representing "now" in the given - timezone. - - :param tz: (optional) A :ref:`timezone expression `. Defaults to local time. - - Usage:: - - >>> import arrow - >>> arrow.now() - - - >>> arrow.now('US/Pacific') - - - >>> arrow.now('+02:00') - - - >>> arrow.now('local') - - """ - - if tz is None: - tz = dateutil_tz.tzlocal() - elif not isinstance(tz, dt_tzinfo): - tz = parser.TzinfoParser.parse(tz) - - return self.type.now(tz) diff --git a/client/ayon_core/vendor/python/python_2/arrow/formatter.py b/client/ayon_core/vendor/python/python_2/arrow/formatter.py deleted file mode 100644 index 9f9d7a44da..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/formatter.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division - -import calendar -import re - -from dateutil import tz as dateutil_tz - -from arrow import locales, util - -FORMAT_ATOM = "YYYY-MM-DD HH:mm:ssZZ" -FORMAT_COOKIE = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ" -FORMAT_RFC822 = "ddd, DD MMM YY HH:mm:ss Z" -FORMAT_RFC850 = "dddd, DD-MMM-YY HH:mm:ss ZZZ" -FORMAT_RFC1036 = "ddd, DD MMM YY HH:mm:ss Z" -FORMAT_RFC1123 = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_RFC2822 = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_RFC3339 = "YYYY-MM-DD HH:mm:ssZZ" -FORMAT_RSS = "ddd, DD MMM YYYY HH:mm:ss Z" -FORMAT_W3C = "YYYY-MM-DD HH:mm:ssZZ" - - -class DateTimeFormatter(object): - - # This pattern matches characters enclosed in square brackets are matched as - # an atomic group. For more info on atomic groups and how to they are - # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578 - - _FORMAT_RE = re.compile( - r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)" - ) - - def __init__(self, locale="en_us"): - - self.locale = locales.get_locale(locale) - - def format(cls, dt, fmt): - - return cls._FORMAT_RE.sub(lambda m: cls._format_token(dt, m.group(0)), fmt) - - def _format_token(self, dt, token): - - if token and token.startswith("[") and token.endswith("]"): - return token[1:-1] - - if token == "YYYY": - return self.locale.year_full(dt.year) - if token == "YY": - return self.locale.year_abbreviation(dt.year) - - if token == "MMMM": - return self.locale.month_name(dt.month) - if token == "MMM": - return self.locale.month_abbreviation(dt.month) - if token == "MM": - return "{:02d}".format(dt.month) - if token == "M": - return str(dt.month) - - if token == "DDDD": - return "{:03d}".format(dt.timetuple().tm_yday) - if token == "DDD": - return str(dt.timetuple().tm_yday) - if token == "DD": - return "{:02d}".format(dt.day) - if token == "D": - return str(dt.day) - - if token == "Do": - return self.locale.ordinal_number(dt.day) - - if token == "dddd": - return self.locale.day_name(dt.isoweekday()) - if token == "ddd": - return self.locale.day_abbreviation(dt.isoweekday()) - if token == "d": - return str(dt.isoweekday()) - - if token == "HH": - return "{:02d}".format(dt.hour) - if token == "H": - return str(dt.hour) - if token == "hh": - return "{:02d}".format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) - if token == "h": - return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12)) - - if token == "mm": - return "{:02d}".format(dt.minute) - if token == "m": - return str(dt.minute) - - if token == "ss": - return "{:02d}".format(dt.second) - if token == "s": - return str(dt.second) - - if token == "SSSSSS": - return str("{:06d}".format(int(dt.microsecond))) - if token == "SSSSS": - return str("{:05d}".format(int(dt.microsecond / 10))) - if token == "SSSS": - return str("{:04d}".format(int(dt.microsecond / 100))) - if token == "SSS": - return str("{:03d}".format(int(dt.microsecond / 1000))) - if token == "SS": - return str("{:02d}".format(int(dt.microsecond / 10000))) - if token == "S": - return str(int(dt.microsecond / 100000)) - - if token == "X": - # TODO: replace with a call to dt.timestamp() when we drop Python 2.7 - return str(calendar.timegm(dt.utctimetuple())) - - if token == "x": - # TODO: replace with a call to dt.timestamp() when we drop Python 2.7 - ts = calendar.timegm(dt.utctimetuple()) + (dt.microsecond / 1000000) - return str(int(ts * 1000000)) - - if token == "ZZZ": - return dt.tzname() - - if token in ["ZZ", "Z"]: - separator = ":" if token == "ZZ" else "" - tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo - total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60) - - sign = "+" if total_minutes >= 0 else "-" - total_minutes = abs(total_minutes) - hour, minute = divmod(total_minutes, 60) - - return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute) - - if token in ("a", "A"): - return self.locale.meridian(dt.hour, token) - - if token == "W": - year, week, day = dt.isocalendar() - return "{}-W{:02d}-{}".format(year, week, day) diff --git a/client/ayon_core/vendor/python/python_2/arrow/locales.py b/client/ayon_core/vendor/python/python_2/arrow/locales.py deleted file mode 100644 index 6833da5a78..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/locales.py +++ /dev/null @@ -1,4267 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import inspect -import sys -from math import trunc - - -def get_locale(name): - """Returns an appropriate :class:`Locale ` - corresponding to an inpute locale name. - - :param name: the name of the locale. - - """ - - locale_cls = _locales.get(name.lower()) - - if locale_cls is None: - raise ValueError("Unsupported locale '{}'".format(name)) - - return locale_cls() - - -def get_locale_by_class_name(name): - """Returns an appropriate :class:`Locale ` - corresponding to an locale class name. - - :param name: the name of the locale class. - - """ - locale_cls = globals().get(name) - - if locale_cls is None: - raise ValueError("Unsupported locale '{}'".format(name)) - - return locale_cls() - - -# base locale type. - - -class Locale(object): - """ Represents locale-specific data and functionality. """ - - names = [] - - timeframes = { - "now": "", - "second": "", - "seconds": "", - "minute": "", - "minutes": "", - "hour": "", - "hours": "", - "day": "", - "days": "", - "week": "", - "weeks": "", - "month": "", - "months": "", - "year": "", - "years": "", - } - - meridians = {"am": "", "pm": "", "AM": "", "PM": ""} - - past = None - future = None - and_word = None - - month_names = [] - month_abbreviations = [] - - day_names = [] - day_abbreviations = [] - - ordinal_day_re = r"(\d+)" - - def __init__(self): - - self._month_name_to_ordinal = None - - def describe(self, timeframe, delta=0, only_distance=False): - """Describes a delta within a timeframe in plain language. - - :param timeframe: a string representing a timeframe. - :param delta: a quantity representing a delta in a timeframe. - :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - """ - - humanized = self._format_timeframe(timeframe, delta) - if not only_distance: - humanized = self._format_relative(humanized, timeframe, delta) - - return humanized - - def describe_multi(self, timeframes, only_distance=False): - """Describes a delta within multiple timeframes in plain language. - - :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. - :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords - """ - - humanized = "" - for index, (timeframe, delta) in enumerate(timeframes): - humanized += self._format_timeframe(timeframe, delta) - if index == len(timeframes) - 2 and self.and_word: - humanized += " " + self.and_word + " " - elif index < len(timeframes) - 1: - humanized += " " - - if not only_distance: - humanized = self._format_relative(humanized, timeframe, delta) - - return humanized - - def day_name(self, day): - """Returns the day name for a specified day of the week. - - :param day: the ``int`` day of the week (1-7). - - """ - - return self.day_names[day] - - def day_abbreviation(self, day): - """Returns the day abbreviation for a specified day of the week. - - :param day: the ``int`` day of the week (1-7). - - """ - - return self.day_abbreviations[day] - - def month_name(self, month): - """Returns the month name for a specified month of the year. - - :param month: the ``int`` month of the year (1-12). - - """ - - return self.month_names[month] - - def month_abbreviation(self, month): - """Returns the month abbreviation for a specified month of the year. - - :param month: the ``int`` month of the year (1-12). - - """ - - return self.month_abbreviations[month] - - def month_number(self, name): - """Returns the month number for a month specified by name or abbreviation. - - :param name: the month name or abbreviation. - - """ - - if self._month_name_to_ordinal is None: - self._month_name_to_ordinal = self._name_to_ordinal(self.month_names) - self._month_name_to_ordinal.update( - self._name_to_ordinal(self.month_abbreviations) - ) - - return self._month_name_to_ordinal.get(name) - - def year_full(self, year): - """Returns the year for specific locale if available - - :param name: the ``int`` year (4-digit) - """ - return "{:04d}".format(year) - - def year_abbreviation(self, year): - """Returns the year for specific locale if available - - :param name: the ``int`` year (4-digit) - """ - return "{:04d}".format(year)[2:] - - def meridian(self, hour, token): - """Returns the meridian indicator for a specified hour and format token. - - :param hour: the ``int`` hour of the day. - :param token: the format token. - """ - - if token == "a": - return self.meridians["am"] if hour < 12 else self.meridians["pm"] - if token == "A": - return self.meridians["AM"] if hour < 12 else self.meridians["PM"] - - def ordinal_number(self, n): - """Returns the ordinal format of a given integer - - :param n: an integer - """ - return self._ordinal_number(n) - - def _ordinal_number(self, n): - return "{}".format(n) - - def _name_to_ordinal(self, lst): - return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:]))) - - def _format_timeframe(self, timeframe, delta): - return self.timeframes[timeframe].format(trunc(abs(delta))) - - def _format_relative(self, humanized, timeframe, delta): - - if timeframe == "now": - return humanized - - direction = self.past if delta < 0 else self.future - - return direction.format(humanized) - - -# base locale type implementations. - - -class EnglishLocale(Locale): - - names = [ - "en", - "en_us", - "en_gb", - "en_au", - "en_be", - "en_jp", - "en_za", - "en_ca", - "en_ph", - ] - - past = "{0} ago" - future = "in {0}" - and_word = "and" - - timeframes = { - "now": "just now", - "second": "a second", - "seconds": "{0} seconds", - "minute": "a minute", - "minutes": "{0} minutes", - "hour": "an hour", - "hours": "{0} hours", - "day": "a day", - "days": "{0} days", - "week": "a week", - "weeks": "{0} weeks", - "month": "a month", - "months": "{0} months", - "year": "a year", - "years": "{0} years", - } - - meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - - month_names = [ - "", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ] - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ] - - day_names = [ - "", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ] - day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - - ordinal_day_re = r"((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))" - - def _ordinal_number(self, n): - if n % 100 not in (11, 12, 13): - remainder = abs(n) % 10 - if remainder == 1: - return "{}st".format(n) - elif remainder == 2: - return "{}nd".format(n) - elif remainder == 3: - return "{}rd".format(n) - return "{}th".format(n) - - def describe(self, timeframe, delta=0, only_distance=False): - """Describes a delta within a timeframe in plain language. - - :param timeframe: a string representing a timeframe. - :param delta: a quantity representing a delta in a timeframe. - :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - """ - - humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance) - if only_distance and timeframe == "now": - humanized = "instantly" - - return humanized - - -class ItalianLocale(Locale): - names = ["it", "it_it"] - past = "{0} fa" - future = "tra {0}" - and_word = "e" - - timeframes = { - "now": "adesso", - "second": "un secondo", - "seconds": "{0} qualche secondo", - "minute": "un minuto", - "minutes": "{0} minuti", - "hour": "un'ora", - "hours": "{0} ore", - "day": "un giorno", - "days": "{0} giorni", - "week": "una settimana,", - "weeks": "{0} settimane", - "month": "un mese", - "months": "{0} mesi", - "year": "un anno", - "years": "{0} anni", - } - - month_names = [ - "", - "gennaio", - "febbraio", - "marzo", - "aprile", - "maggio", - "giugno", - "luglio", - "agosto", - "settembre", - "ottobre", - "novembre", - "dicembre", - ] - month_abbreviations = [ - "", - "gen", - "feb", - "mar", - "apr", - "mag", - "giu", - "lug", - "ago", - "set", - "ott", - "nov", - "dic", - ] - - day_names = [ - "", - "lunedì", - "martedì", - "mercoledì", - "giovedì", - "venerdì", - "sabato", - "domenica", - ] - day_abbreviations = ["", "lun", "mar", "mer", "gio", "ven", "sab", "dom"] - - ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" - - def _ordinal_number(self, n): - return "{}º".format(n) - - -class SpanishLocale(Locale): - names = ["es", "es_es"] - past = "hace {0}" - future = "en {0}" - and_word = "y" - - timeframes = { - "now": "ahora", - "second": "un segundo", - "seconds": "{0} segundos", - "minute": "un minuto", - "minutes": "{0} minutos", - "hour": "una hora", - "hours": "{0} horas", - "day": "un día", - "days": "{0} días", - "week": "una semana", - "weeks": "{0} semanas", - "month": "un mes", - "months": "{0} meses", - "year": "un año", - "years": "{0} años", - } - - meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - - month_names = [ - "", - "enero", - "febrero", - "marzo", - "abril", - "mayo", - "junio", - "julio", - "agosto", - "septiembre", - "octubre", - "noviembre", - "diciembre", - ] - month_abbreviations = [ - "", - "ene", - "feb", - "mar", - "abr", - "may", - "jun", - "jul", - "ago", - "sep", - "oct", - "nov", - "dic", - ] - - day_names = [ - "", - "lunes", - "martes", - "miércoles", - "jueves", - "viernes", - "sábado", - "domingo", - ] - day_abbreviations = ["", "lun", "mar", "mie", "jue", "vie", "sab", "dom"] - - ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])" - - def _ordinal_number(self, n): - return "{}º".format(n) - - -class FrenchBaseLocale(Locale): - - past = "il y a {0}" - future = "dans {0}" - and_word = "et" - - timeframes = { - "now": "maintenant", - "second": "une seconde", - "seconds": "{0} quelques secondes", - "minute": "une minute", - "minutes": "{0} minutes", - "hour": "une heure", - "hours": "{0} heures", - "day": "un jour", - "days": "{0} jours", - "week": "une semaine", - "weeks": "{0} semaines", - "month": "un mois", - "months": "{0} mois", - "year": "un an", - "years": "{0} ans", - } - - month_names = [ - "", - "janvier", - "février", - "mars", - "avril", - "mai", - "juin", - "juillet", - "août", - "septembre", - "octobre", - "novembre", - "décembre", - ] - - day_names = [ - "", - "lundi", - "mardi", - "mercredi", - "jeudi", - "vendredi", - "samedi", - "dimanche", - ] - day_abbreviations = ["", "lun", "mar", "mer", "jeu", "ven", "sam", "dim"] - - ordinal_day_re = ( - r"((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)" - ) - - def _ordinal_number(self, n): - if abs(n) == 1: - return "{}er".format(n) - return "{}e".format(n) - - -class FrenchLocale(FrenchBaseLocale, Locale): - - names = ["fr", "fr_fr"] - - month_abbreviations = [ - "", - "janv", - "févr", - "mars", - "avr", - "mai", - "juin", - "juil", - "août", - "sept", - "oct", - "nov", - "déc", - ] - - -class FrenchCanadianLocale(FrenchBaseLocale, Locale): - - names = ["fr_ca"] - - month_abbreviations = [ - "", - "janv", - "févr", - "mars", - "avr", - "mai", - "juin", - "juill", - "août", - "sept", - "oct", - "nov", - "déc", - ] - - -class GreekLocale(Locale): - - names = ["el", "el_gr"] - - past = "{0} πριν" - future = "σε {0}" - and_word = "και" - - timeframes = { - "now": "τώρα", - "second": "ένα δεύτερο", - "seconds": "{0} δευτερόλεπτα", - "minute": "ένα λεπτό", - "minutes": "{0} λεπτά", - "hour": "μία ώρα", - "hours": "{0} ώρες", - "day": "μία μέρα", - "days": "{0} μέρες", - "month": "ένα μήνα", - "months": "{0} μήνες", - "year": "ένα χρόνο", - "years": "{0} χρόνια", - } - - month_names = [ - "", - "Ιανουαρίου", - "Φεβρουαρίου", - "Μαρτίου", - "Απριλίου", - "Μαΐου", - "Ιουνίου", - "Ιουλίου", - "Αυγούστου", - "Σεπτεμβρίου", - "Οκτωβρίου", - "Νοεμβρίου", - "Δεκεμβρίου", - ] - month_abbreviations = [ - "", - "Ιαν", - "Φεβ", - "Μαρ", - "Απρ", - "Μαϊ", - "Ιον", - "Ιολ", - "Αυγ", - "Σεπ", - "Οκτ", - "Νοε", - "Δεκ", - ] - - day_names = [ - "", - "Δευτέρα", - "Τρίτη", - "Τετάρτη", - "Πέμπτη", - "Παρασκευή", - "Σάββατο", - "Κυριακή", - ] - day_abbreviations = ["", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ", "Κυρ"] - - -class JapaneseLocale(Locale): - - names = ["ja", "ja_jp"] - - past = "{0}前" - future = "{0}後" - - timeframes = { - "now": "現在", - "second": "二番目の", - "seconds": "{0}数秒", - "minute": "1分", - "minutes": "{0}分", - "hour": "1時間", - "hours": "{0}時間", - "day": "1日", - "days": "{0}日", - "week": "1週間", - "weeks": "{0}週間", - "month": "1ヶ月", - "months": "{0}ヶ月", - "year": "1年", - "years": "{0}年", - } - - month_names = [ - "", - "1月", - "2月", - "3月", - "4月", - "5月", - "6月", - "7月", - "8月", - "9月", - "10月", - "11月", - "12月", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"] - day_abbreviations = ["", "月", "火", "水", "木", "金", "土", "日"] - - -class SwedishLocale(Locale): - - names = ["sv", "sv_se"] - - past = "för {0} sen" - future = "om {0}" - and_word = "och" - - timeframes = { - "now": "just nu", - "second": "en sekund", - "seconds": "{0} några sekunder", - "minute": "en minut", - "minutes": "{0} minuter", - "hour": "en timme", - "hours": "{0} timmar", - "day": "en dag", - "days": "{0} dagar", - "week": "en vecka", - "weeks": "{0} veckor", - "month": "en månad", - "months": "{0} månader", - "year": "ett år", - "years": "{0} år", - } - - month_names = [ - "", - "januari", - "februari", - "mars", - "april", - "maj", - "juni", - "juli", - "augusti", - "september", - "oktober", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "maj", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "måndag", - "tisdag", - "onsdag", - "torsdag", - "fredag", - "lördag", - "söndag", - ] - day_abbreviations = ["", "mån", "tis", "ons", "tor", "fre", "lör", "sön"] - - -class FinnishLocale(Locale): - - names = ["fi", "fi_fi"] - - # The finnish grammar is very complex, and its hard to convert - # 1-to-1 to something like English. - - past = "{0} sitten" - future = "{0} kuluttua" - - timeframes = { - "now": ["juuri nyt", "juuri nyt"], - "second": ["sekunti", "sekunti"], - "seconds": ["{0} muutama sekunti", "{0} muutaman sekunnin"], - "minute": ["minuutti", "minuutin"], - "minutes": ["{0} minuuttia", "{0} minuutin"], - "hour": ["tunti", "tunnin"], - "hours": ["{0} tuntia", "{0} tunnin"], - "day": ["päivä", "päivä"], - "days": ["{0} päivää", "{0} päivän"], - "month": ["kuukausi", "kuukauden"], - "months": ["{0} kuukautta", "{0} kuukauden"], - "year": ["vuosi", "vuoden"], - "years": ["{0} vuotta", "{0} vuoden"], - } - - # Months and days are lowercase in Finnish - month_names = [ - "", - "tammikuu", - "helmikuu", - "maaliskuu", - "huhtikuu", - "toukokuu", - "kesäkuu", - "heinäkuu", - "elokuu", - "syyskuu", - "lokakuu", - "marraskuu", - "joulukuu", - ] - - month_abbreviations = [ - "", - "tammi", - "helmi", - "maalis", - "huhti", - "touko", - "kesä", - "heinä", - "elo", - "syys", - "loka", - "marras", - "joulu", - ] - - day_names = [ - "", - "maanantai", - "tiistai", - "keskiviikko", - "torstai", - "perjantai", - "lauantai", - "sunnuntai", - ] - - day_abbreviations = ["", "ma", "ti", "ke", "to", "pe", "la", "su"] - - def _format_timeframe(self, timeframe, delta): - return ( - self.timeframes[timeframe][0].format(abs(delta)), - self.timeframes[timeframe][1].format(abs(delta)), - ) - - def _format_relative(self, humanized, timeframe, delta): - if timeframe == "now": - return humanized[0] - - direction = self.past if delta < 0 else self.future - which = 0 if delta < 0 else 1 - - return direction.format(humanized[which]) - - def _ordinal_number(self, n): - return "{}.".format(n) - - -class ChineseCNLocale(Locale): - - names = ["zh", "zh_cn"] - - past = "{0}前" - future = "{0}后" - - timeframes = { - "now": "刚才", - "second": "一秒", - "seconds": "{0}秒", - "minute": "1分钟", - "minutes": "{0}分钟", - "hour": "1小时", - "hours": "{0}小时", - "day": "1天", - "days": "{0}天", - "week": "一周", - "weeks": "{0}周", - "month": "1个月", - "months": "{0}个月", - "year": "1年", - "years": "{0}年", - } - - month_names = [ - "", - "一月", - "二月", - "三月", - "四月", - "五月", - "六月", - "七月", - "八月", - "九月", - "十月", - "十一月", - "十二月", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] - day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] - - -class ChineseTWLocale(Locale): - - names = ["zh_tw"] - - past = "{0}前" - future = "{0}後" - and_word = "和" - - timeframes = { - "now": "剛才", - "second": "1秒", - "seconds": "{0}秒", - "minute": "1分鐘", - "minutes": "{0}分鐘", - "hour": "1小時", - "hours": "{0}小時", - "day": "1天", - "days": "{0}天", - "week": "1週", - "weeks": "{0}週", - "month": "1個月", - "months": "{0}個月", - "year": "1年", - "years": "{0}年", - } - - month_names = [ - "", - "1月", - "2月", - "3月", - "4月", - "5月", - "6月", - "7月", - "8月", - "9月", - "10月", - "11月", - "12月", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "週一", "週二", "週三", "週四", "週五", "週六", "週日"] - day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] - - -class HongKongLocale(Locale): - - names = ["zh_hk"] - - past = "{0}前" - future = "{0}後" - - timeframes = { - "now": "剛才", - "second": "1秒", - "seconds": "{0}秒", - "minute": "1分鐘", - "minutes": "{0}分鐘", - "hour": "1小時", - "hours": "{0}小時", - "day": "1天", - "days": "{0}天", - "week": "1星期", - "weeks": "{0}星期", - "month": "1個月", - "months": "{0}個月", - "year": "1年", - "years": "{0}年", - } - - month_names = [ - "", - "1月", - "2月", - "3月", - "4月", - "5月", - "6月", - "7月", - "8月", - "9月", - "10月", - "11月", - "12月", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"] - day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"] - - -class KoreanLocale(Locale): - - names = ["ko", "ko_kr"] - - past = "{0} 전" - future = "{0} 후" - - timeframes = { - "now": "지금", - "second": "1초", - "seconds": "{0}초", - "minute": "1분", - "minutes": "{0}분", - "hour": "한시간", - "hours": "{0}시간", - "day": "하루", - "days": "{0}일", - "week": "1주", - "weeks": "{0}주", - "month": "한달", - "months": "{0}개월", - "year": "1년", - "years": "{0}년", - } - - special_dayframes = { - -3: "그끄제", - -2: "그제", - -1: "어제", - 1: "내일", - 2: "모레", - 3: "글피", - 4: "그글피", - } - - special_yearframes = {-2: "제작년", -1: "작년", 1: "내년", 2: "내후년"} - - month_names = [ - "", - "1월", - "2월", - "3월", - "4월", - "5월", - "6월", - "7월", - "8월", - "9월", - "10월", - "11월", - "12월", - ] - month_abbreviations = [ - "", - " 1", - " 2", - " 3", - " 4", - " 5", - " 6", - " 7", - " 8", - " 9", - "10", - "11", - "12", - ] - - day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"] - day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"] - - def _ordinal_number(self, n): - ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"] - if n < len(ordinals): - return "{}번째".format(ordinals[n]) - return "{}번째".format(n) - - def _format_relative(self, humanized, timeframe, delta): - if timeframe in ("day", "days"): - special = self.special_dayframes.get(delta) - if special: - return special - elif timeframe in ("year", "years"): - special = self.special_yearframes.get(delta) - if special: - return special - - return super(KoreanLocale, self)._format_relative(humanized, timeframe, delta) - - -# derived locale types & implementations. -class DutchLocale(Locale): - - names = ["nl", "nl_nl"] - - past = "{0} geleden" - future = "over {0}" - - timeframes = { - "now": "nu", - "second": "een seconde", - "seconds": "{0} seconden", - "minute": "een minuut", - "minutes": "{0} minuten", - "hour": "een uur", - "hours": "{0} uur", - "day": "een dag", - "days": "{0} dagen", - "week": "een week", - "weeks": "{0} weken", - "month": "een maand", - "months": "{0} maanden", - "year": "een jaar", - "years": "{0} jaar", - } - - # In Dutch names of months and days are not starting with a capital letter - # like in the English language. - month_names = [ - "", - "januari", - "februari", - "maart", - "april", - "mei", - "juni", - "juli", - "augustus", - "september", - "oktober", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mrt", - "apr", - "mei", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "maandag", - "dinsdag", - "woensdag", - "donderdag", - "vrijdag", - "zaterdag", - "zondag", - ] - day_abbreviations = ["", "ma", "di", "wo", "do", "vr", "za", "zo"] - - -class SlavicBaseLocale(Locale): - def _format_timeframe(self, timeframe, delta): - - form = self.timeframes[timeframe] - delta = abs(delta) - - if isinstance(form, list): - - if delta % 10 == 1 and delta % 100 != 11: - form = form[0] - elif 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[1] - else: - form = form[2] - - return form.format(delta) - - -class BelarusianLocale(SlavicBaseLocale): - - names = ["be", "be_by"] - - past = "{0} таму" - future = "праз {0}" - - timeframes = { - "now": "зараз", - "second": "секунду", - "seconds": "{0} некалькі секунд", - "minute": "хвіліну", - "minutes": ["{0} хвіліну", "{0} хвіліны", "{0} хвілін"], - "hour": "гадзіну", - "hours": ["{0} гадзіну", "{0} гадзіны", "{0} гадзін"], - "day": "дзень", - "days": ["{0} дзень", "{0} дні", "{0} дзён"], - "month": "месяц", - "months": ["{0} месяц", "{0} месяцы", "{0} месяцаў"], - "year": "год", - "years": ["{0} год", "{0} гады", "{0} гадоў"], - } - - month_names = [ - "", - "студзеня", - "лютага", - "сакавіка", - "красавіка", - "траўня", - "чэрвеня", - "ліпеня", - "жніўня", - "верасня", - "кастрычніка", - "лістапада", - "снежня", - ] - month_abbreviations = [ - "", - "студ", - "лют", - "сак", - "крас", - "трав", - "чэрв", - "ліп", - "жнів", - "вер", - "каст", - "ліст", - "снеж", - ] - - day_names = [ - "", - "панядзелак", - "аўторак", - "серада", - "чацвер", - "пятніца", - "субота", - "нядзеля", - ] - day_abbreviations = ["", "пн", "ат", "ср", "чц", "пт", "сб", "нд"] - - -class PolishLocale(SlavicBaseLocale): - - names = ["pl", "pl_pl"] - - past = "{0} temu" - future = "za {0}" - - # The nouns should be in genitive case (Polish: "dopełniacz") - # in order to correctly form `past` & `future` expressions. - timeframes = { - "now": "teraz", - "second": "sekundę", - "seconds": ["{0} sekund", "{0} sekundy", "{0} sekund"], - "minute": "minutę", - "minutes": ["{0} minut", "{0} minuty", "{0} minut"], - "hour": "godzinę", - "hours": ["{0} godzin", "{0} godziny", "{0} godzin"], - "day": "dzień", - "days": "{0} dni", - "week": "tydzień", - "weeks": ["{0} tygodni", "{0} tygodnie", "{0} tygodni"], - "month": "miesiąc", - "months": ["{0} miesięcy", "{0} miesiące", "{0} miesięcy"], - "year": "rok", - "years": ["{0} lat", "{0} lata", "{0} lat"], - } - - month_names = [ - "", - "styczeń", - "luty", - "marzec", - "kwiecień", - "maj", - "czerwiec", - "lipiec", - "sierpień", - "wrzesień", - "październik", - "listopad", - "grudzień", - ] - month_abbreviations = [ - "", - "sty", - "lut", - "mar", - "kwi", - "maj", - "cze", - "lip", - "sie", - "wrz", - "paź", - "lis", - "gru", - ] - - day_names = [ - "", - "poniedziałek", - "wtorek", - "środa", - "czwartek", - "piątek", - "sobota", - "niedziela", - ] - day_abbreviations = ["", "Pn", "Wt", "Śr", "Czw", "Pt", "So", "Nd"] - - -class RussianLocale(SlavicBaseLocale): - - names = ["ru", "ru_ru"] - - past = "{0} назад" - future = "через {0}" - - timeframes = { - "now": "сейчас", - "second": "Второй", - "seconds": "{0} несколько секунд", - "minute": "минуту", - "minutes": ["{0} минуту", "{0} минуты", "{0} минут"], - "hour": "час", - "hours": ["{0} час", "{0} часа", "{0} часов"], - "day": "день", - "days": ["{0} день", "{0} дня", "{0} дней"], - "week": "неделю", - "weeks": ["{0} неделю", "{0} недели", "{0} недель"], - "month": "месяц", - "months": ["{0} месяц", "{0} месяца", "{0} месяцев"], - "year": "год", - "years": ["{0} год", "{0} года", "{0} лет"], - } - - month_names = [ - "", - "января", - "февраля", - "марта", - "апреля", - "мая", - "июня", - "июля", - "августа", - "сентября", - "октября", - "ноября", - "декабря", - ] - month_abbreviations = [ - "", - "янв", - "фев", - "мар", - "апр", - "май", - "июн", - "июл", - "авг", - "сен", - "окт", - "ноя", - "дек", - ] - - day_names = [ - "", - "понедельник", - "вторник", - "среда", - "четверг", - "пятница", - "суббота", - "воскресенье", - ] - day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "вс"] - - -class AfrikaansLocale(Locale): - - names = ["af", "af_nl"] - - past = "{0} gelede" - future = "in {0}" - - timeframes = { - "now": "nou", - "second": "n sekonde", - "seconds": "{0} sekondes", - "minute": "minuut", - "minutes": "{0} minute", - "hour": "uur", - "hours": "{0} ure", - "day": "een dag", - "days": "{0} dae", - "month": "een maand", - "months": "{0} maande", - "year": "een jaar", - "years": "{0} jaar", - } - - month_names = [ - "", - "Januarie", - "Februarie", - "Maart", - "April", - "Mei", - "Junie", - "Julie", - "Augustus", - "September", - "Oktober", - "November", - "Desember", - ] - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mrt", - "Apr", - "Mei", - "Jun", - "Jul", - "Aug", - "Sep", - "Okt", - "Nov", - "Des", - ] - - day_names = [ - "", - "Maandag", - "Dinsdag", - "Woensdag", - "Donderdag", - "Vrydag", - "Saterdag", - "Sondag", - ] - day_abbreviations = ["", "Ma", "Di", "Wo", "Do", "Vr", "Za", "So"] - - -class BulgarianLocale(SlavicBaseLocale): - - names = ["bg", "bg_BG"] - - past = "{0} назад" - future = "напред {0}" - - timeframes = { - "now": "сега", - "second": "секунда", - "seconds": "{0} няколко секунди", - "minute": "минута", - "minutes": ["{0} минута", "{0} минути", "{0} минути"], - "hour": "час", - "hours": ["{0} час", "{0} часа", "{0} часа"], - "day": "ден", - "days": ["{0} ден", "{0} дни", "{0} дни"], - "month": "месец", - "months": ["{0} месец", "{0} месеца", "{0} месеца"], - "year": "година", - "years": ["{0} година", "{0} години", "{0} години"], - } - - month_names = [ - "", - "януари", - "февруари", - "март", - "април", - "май", - "юни", - "юли", - "август", - "септември", - "октомври", - "ноември", - "декември", - ] - month_abbreviations = [ - "", - "ян", - "февр", - "март", - "апр", - "май", - "юни", - "юли", - "авг", - "септ", - "окт", - "ноем", - "дек", - ] - - day_names = [ - "", - "понеделник", - "вторник", - "сряда", - "четвъртък", - "петък", - "събота", - "неделя", - ] - day_abbreviations = ["", "пон", "вт", "ср", "четв", "пет", "съб", "нед"] - - -class UkrainianLocale(SlavicBaseLocale): - - names = ["ua", "uk_ua"] - - past = "{0} тому" - future = "за {0}" - - timeframes = { - "now": "зараз", - "second": "секунда", - "seconds": "{0} кілька секунд", - "minute": "хвилину", - "minutes": ["{0} хвилину", "{0} хвилини", "{0} хвилин"], - "hour": "годину", - "hours": ["{0} годину", "{0} години", "{0} годин"], - "day": "день", - "days": ["{0} день", "{0} дні", "{0} днів"], - "month": "місяць", - "months": ["{0} місяць", "{0} місяці", "{0} місяців"], - "year": "рік", - "years": ["{0} рік", "{0} роки", "{0} років"], - } - - month_names = [ - "", - "січня", - "лютого", - "березня", - "квітня", - "травня", - "червня", - "липня", - "серпня", - "вересня", - "жовтня", - "листопада", - "грудня", - ] - month_abbreviations = [ - "", - "січ", - "лют", - "бер", - "квіт", - "трав", - "черв", - "лип", - "серп", - "вер", - "жовт", - "лист", - "груд", - ] - - day_names = [ - "", - "понеділок", - "вівторок", - "середа", - "четвер", - "п’ятниця", - "субота", - "неділя", - ] - day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "нд"] - - -class MacedonianLocale(SlavicBaseLocale): - names = ["mk", "mk_mk"] - - past = "пред {0}" - future = "за {0}" - - timeframes = { - "now": "сега", - "second": "една секунда", - "seconds": ["{0} секунда", "{0} секунди", "{0} секунди"], - "minute": "една минута", - "minutes": ["{0} минута", "{0} минути", "{0} минути"], - "hour": "еден саат", - "hours": ["{0} саат", "{0} саати", "{0} саати"], - "day": "еден ден", - "days": ["{0} ден", "{0} дена", "{0} дена"], - "week": "една недела", - "weeks": ["{0} недела", "{0} недели", "{0} недели"], - "month": "еден месец", - "months": ["{0} месец", "{0} месеци", "{0} месеци"], - "year": "една година", - "years": ["{0} година", "{0} години", "{0} години"], - } - - meridians = {"am": "дп", "pm": "пп", "AM": "претпладне", "PM": "попладне"} - - month_names = [ - "", - "Јануари", - "Февруари", - "Март", - "Април", - "Мај", - "Јуни", - "Јули", - "Август", - "Септември", - "Октомври", - "Ноември", - "Декември", - ] - month_abbreviations = [ - "", - "Јан", - "Фев", - "Мар", - "Апр", - "Мај", - "Јун", - "Јул", - "Авг", - "Септ", - "Окт", - "Ноем", - "Декем", - ] - - day_names = [ - "", - "Понеделник", - "Вторник", - "Среда", - "Четврток", - "Петок", - "Сабота", - "Недела", - ] - day_abbreviations = [ - "", - "Пон", - "Вт", - "Сре", - "Чет", - "Пет", - "Саб", - "Нед", - ] - - -class GermanBaseLocale(Locale): - - past = "vor {0}" - future = "in {0}" - and_word = "und" - - timeframes = { - "now": "gerade eben", - "second": "eine Sekunde", - "seconds": "{0} Sekunden", - "minute": "einer Minute", - "minutes": "{0} Minuten", - "hour": "einer Stunde", - "hours": "{0} Stunden", - "day": "einem Tag", - "days": "{0} Tagen", - "week": "einer Woche", - "weeks": "{0} Wochen", - "month": "einem Monat", - "months": "{0} Monaten", - "year": "einem Jahr", - "years": "{0} Jahren", - } - - timeframes_only_distance = timeframes.copy() - timeframes_only_distance["minute"] = "eine Minute" - timeframes_only_distance["hour"] = "eine Stunde" - timeframes_only_distance["day"] = "ein Tag" - timeframes_only_distance["week"] = "eine Woche" - timeframes_only_distance["month"] = "ein Monat" - timeframes_only_distance["year"] = "ein Jahr" - - month_names = [ - "", - "Januar", - "Februar", - "März", - "April", - "Mai", - "Juni", - "Juli", - "August", - "September", - "Oktober", - "November", - "Dezember", - ] - - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mär", - "Apr", - "Mai", - "Jun", - "Jul", - "Aug", - "Sep", - "Okt", - "Nov", - "Dez", - ] - - day_names = [ - "", - "Montag", - "Dienstag", - "Mittwoch", - "Donnerstag", - "Freitag", - "Samstag", - "Sonntag", - ] - - day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] - - def _ordinal_number(self, n): - return "{}.".format(n) - - def describe(self, timeframe, delta=0, only_distance=False): - """Describes a delta within a timeframe in plain language. - - :param timeframe: a string representing a timeframe. - :param delta: a quantity representing a delta in a timeframe. - :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords - """ - - if not only_distance: - return super(GermanBaseLocale, self).describe( - timeframe, delta, only_distance - ) - - # German uses a different case without 'in' or 'ago' - humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta))) - - return humanized - - -class GermanLocale(GermanBaseLocale, Locale): - - names = ["de", "de_de"] - - -class SwissLocale(GermanBaseLocale, Locale): - - names = ["de_ch"] - - -class AustrianLocale(GermanBaseLocale, Locale): - - names = ["de_at"] - - month_names = [ - "", - "Jänner", - "Februar", - "März", - "April", - "Mai", - "Juni", - "Juli", - "August", - "September", - "Oktober", - "November", - "Dezember", - ] - - -class NorwegianLocale(Locale): - - names = ["nb", "nb_no"] - - past = "for {0} siden" - future = "om {0}" - - timeframes = { - "now": "nå nettopp", - "second": "et sekund", - "seconds": "{0} noen sekunder", - "minute": "ett minutt", - "minutes": "{0} minutter", - "hour": "en time", - "hours": "{0} timer", - "day": "en dag", - "days": "{0} dager", - "month": "en måned", - "months": "{0} måneder", - "year": "ett år", - "years": "{0} år", - } - - month_names = [ - "", - "januar", - "februar", - "mars", - "april", - "mai", - "juni", - "juli", - "august", - "september", - "oktober", - "november", - "desember", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "mai", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "des", - ] - - day_names = [ - "", - "mandag", - "tirsdag", - "onsdag", - "torsdag", - "fredag", - "lørdag", - "søndag", - ] - day_abbreviations = ["", "ma", "ti", "on", "to", "fr", "lø", "sø"] - - -class NewNorwegianLocale(Locale): - - names = ["nn", "nn_no"] - - past = "for {0} sidan" - future = "om {0}" - - timeframes = { - "now": "no nettopp", - "second": "et sekund", - "seconds": "{0} nokre sekund", - "minute": "ett minutt", - "minutes": "{0} minutt", - "hour": "ein time", - "hours": "{0} timar", - "day": "ein dag", - "days": "{0} dagar", - "month": "en månad", - "months": "{0} månader", - "year": "eit år", - "years": "{0} år", - } - - month_names = [ - "", - "januar", - "februar", - "mars", - "april", - "mai", - "juni", - "juli", - "august", - "september", - "oktober", - "november", - "desember", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "mai", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "des", - ] - - day_names = [ - "", - "måndag", - "tysdag", - "onsdag", - "torsdag", - "fredag", - "laurdag", - "sundag", - ] - day_abbreviations = ["", "må", "ty", "on", "to", "fr", "la", "su"] - - -class PortugueseLocale(Locale): - names = ["pt", "pt_pt"] - - past = "há {0}" - future = "em {0}" - and_word = "e" - - timeframes = { - "now": "agora", - "second": "um segundo", - "seconds": "{0} segundos", - "minute": "um minuto", - "minutes": "{0} minutos", - "hour": "uma hora", - "hours": "{0} horas", - "day": "um dia", - "days": "{0} dias", - "week": "uma semana", - "weeks": "{0} semanas", - "month": "um mês", - "months": "{0} meses", - "year": "um ano", - "years": "{0} anos", - } - - month_names = [ - "", - "Janeiro", - "Fevereiro", - "Março", - "Abril", - "Maio", - "Junho", - "Julho", - "Agosto", - "Setembro", - "Outubro", - "Novembro", - "Dezembro", - ] - month_abbreviations = [ - "", - "Jan", - "Fev", - "Mar", - "Abr", - "Mai", - "Jun", - "Jul", - "Ago", - "Set", - "Out", - "Nov", - "Dez", - ] - - day_names = [ - "", - "Segunda-feira", - "Terça-feira", - "Quarta-feira", - "Quinta-feira", - "Sexta-feira", - "Sábado", - "Domingo", - ] - day_abbreviations = ["", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab", "Dom"] - - -class BrazilianPortugueseLocale(PortugueseLocale): - names = ["pt_br"] - - past = "faz {0}" - - -class TagalogLocale(Locale): - - names = ["tl", "tl_ph"] - - past = "nakaraang {0}" - future = "{0} mula ngayon" - - timeframes = { - "now": "ngayon lang", - "second": "isang segundo", - "seconds": "{0} segundo", - "minute": "isang minuto", - "minutes": "{0} minuto", - "hour": "isang oras", - "hours": "{0} oras", - "day": "isang araw", - "days": "{0} araw", - "week": "isang linggo", - "weeks": "{0} linggo", - "month": "isang buwan", - "months": "{0} buwan", - "year": "isang taon", - "years": "{0} taon", - } - - month_names = [ - "", - "Enero", - "Pebrero", - "Marso", - "Abril", - "Mayo", - "Hunyo", - "Hulyo", - "Agosto", - "Setyembre", - "Oktubre", - "Nobyembre", - "Disyembre", - ] - month_abbreviations = [ - "", - "Ene", - "Peb", - "Mar", - "Abr", - "May", - "Hun", - "Hul", - "Ago", - "Set", - "Okt", - "Nob", - "Dis", - ] - - day_names = [ - "", - "Lunes", - "Martes", - "Miyerkules", - "Huwebes", - "Biyernes", - "Sabado", - "Linggo", - ] - day_abbreviations = ["", "Lun", "Mar", "Miy", "Huw", "Biy", "Sab", "Lin"] - - meridians = {"am": "nu", "pm": "nh", "AM": "ng umaga", "PM": "ng hapon"} - - def _ordinal_number(self, n): - return "ika-{}".format(n) - - -class VietnameseLocale(Locale): - - names = ["vi", "vi_vn"] - - past = "{0} trước" - future = "{0} nữa" - - timeframes = { - "now": "hiện tại", - "second": "một giây", - "seconds": "{0} giây", - "minute": "một phút", - "minutes": "{0} phút", - "hour": "một giờ", - "hours": "{0} giờ", - "day": "một ngày", - "days": "{0} ngày", - "week": "một tuần", - "weeks": "{0} tuần", - "month": "một tháng", - "months": "{0} tháng", - "year": "một năm", - "years": "{0} năm", - } - - month_names = [ - "", - "Tháng Một", - "Tháng Hai", - "Tháng Ba", - "Tháng Tư", - "Tháng Năm", - "Tháng Sáu", - "Tháng Bảy", - "Tháng Tám", - "Tháng Chín", - "Tháng Mười", - "Tháng Mười Một", - "Tháng Mười Hai", - ] - month_abbreviations = [ - "", - "Tháng 1", - "Tháng 2", - "Tháng 3", - "Tháng 4", - "Tháng 5", - "Tháng 6", - "Tháng 7", - "Tháng 8", - "Tháng 9", - "Tháng 10", - "Tháng 11", - "Tháng 12", - ] - - day_names = [ - "", - "Thứ Hai", - "Thứ Ba", - "Thứ Tư", - "Thứ Năm", - "Thứ Sáu", - "Thứ Bảy", - "Chủ Nhật", - ] - day_abbreviations = ["", "Thứ 2", "Thứ 3", "Thứ 4", "Thứ 5", "Thứ 6", "Thứ 7", "CN"] - - -class TurkishLocale(Locale): - - names = ["tr", "tr_tr"] - - past = "{0} önce" - future = "{0} sonra" - - timeframes = { - "now": "şimdi", - "second": "bir saniye", - "seconds": "{0} saniye", - "minute": "bir dakika", - "minutes": "{0} dakika", - "hour": "bir saat", - "hours": "{0} saat", - "day": "bir gün", - "days": "{0} gün", - "month": "bir ay", - "months": "{0} ay", - "year": "yıl", - "years": "{0} yıl", - } - - month_names = [ - "", - "Ocak", - "Şubat", - "Mart", - "Nisan", - "Mayıs", - "Haziran", - "Temmuz", - "Ağustos", - "Eylül", - "Ekim", - "Kasım", - "Aralık", - ] - month_abbreviations = [ - "", - "Oca", - "Şub", - "Mar", - "Nis", - "May", - "Haz", - "Tem", - "Ağu", - "Eyl", - "Eki", - "Kas", - "Ara", - ] - - day_names = [ - "", - "Pazartesi", - "Salı", - "Çarşamba", - "Perşembe", - "Cuma", - "Cumartesi", - "Pazar", - ] - day_abbreviations = ["", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"] - - -class AzerbaijaniLocale(Locale): - - names = ["az", "az_az"] - - past = "{0} əvvəl" - future = "{0} sonra" - - timeframes = { - "now": "indi", - "second": "saniyə", - "seconds": "{0} saniyə", - "minute": "bir dəqiqə", - "minutes": "{0} dəqiqə", - "hour": "bir saat", - "hours": "{0} saat", - "day": "bir gün", - "days": "{0} gün", - "month": "bir ay", - "months": "{0} ay", - "year": "il", - "years": "{0} il", - } - - month_names = [ - "", - "Yanvar", - "Fevral", - "Mart", - "Aprel", - "May", - "İyun", - "İyul", - "Avqust", - "Sentyabr", - "Oktyabr", - "Noyabr", - "Dekabr", - ] - month_abbreviations = [ - "", - "Yan", - "Fev", - "Mar", - "Apr", - "May", - "İyn", - "İyl", - "Avq", - "Sen", - "Okt", - "Noy", - "Dek", - ] - - day_names = [ - "", - "Bazar ertəsi", - "Çərşənbə axşamı", - "Çərşənbə", - "Cümə axşamı", - "Cümə", - "Şənbə", - "Bazar", - ] - day_abbreviations = ["", "Ber", "Çax", "Çər", "Cax", "Cüm", "Şnb", "Bzr"] - - -class ArabicLocale(Locale): - names = [ - "ar", - "ar_ae", - "ar_bh", - "ar_dj", - "ar_eg", - "ar_eh", - "ar_er", - "ar_km", - "ar_kw", - "ar_ly", - "ar_om", - "ar_qa", - "ar_sa", - "ar_sd", - "ar_so", - "ar_ss", - "ar_td", - "ar_ye", - ] - - past = "منذ {0}" - future = "خلال {0}" - - timeframes = { - "now": "الآن", - "second": "ثانية", - "seconds": {"double": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"}, - "minute": "دقيقة", - "minutes": {"double": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"}, - "hour": "ساعة", - "hours": {"double": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"}, - "day": "يوم", - "days": {"double": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"}, - "month": "شهر", - "months": {"double": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"}, - "year": "سنة", - "years": {"double": "سنتين", "ten": "{0} سنوات", "higher": "{0} سنة"}, - } - - month_names = [ - "", - "يناير", - "فبراير", - "مارس", - "أبريل", - "مايو", - "يونيو", - "يوليو", - "أغسطس", - "سبتمبر", - "أكتوبر", - "نوفمبر", - "ديسمبر", - ] - month_abbreviations = [ - "", - "يناير", - "فبراير", - "مارس", - "أبريل", - "مايو", - "يونيو", - "يوليو", - "أغسطس", - "سبتمبر", - "أكتوبر", - "نوفمبر", - "ديسمبر", - ] - - day_names = [ - "", - "الإثنين", - "الثلاثاء", - "الأربعاء", - "الخميس", - "الجمعة", - "السبت", - "الأحد", - ] - day_abbreviations = ["", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"] - - def _format_timeframe(self, timeframe, delta): - form = self.timeframes[timeframe] - delta = abs(delta) - if isinstance(form, dict): - if delta == 2: - form = form["double"] - elif delta > 2 and delta <= 10: - form = form["ten"] - else: - form = form["higher"] - - return form.format(delta) - - -class LevantArabicLocale(ArabicLocale): - names = ["ar_iq", "ar_jo", "ar_lb", "ar_ps", "ar_sy"] - month_names = [ - "", - "كانون الثاني", - "شباط", - "آذار", - "نيسان", - "أيار", - "حزيران", - "تموز", - "آب", - "أيلول", - "تشرين الأول", - "تشرين الثاني", - "كانون الأول", - ] - month_abbreviations = [ - "", - "كانون الثاني", - "شباط", - "آذار", - "نيسان", - "أيار", - "حزيران", - "تموز", - "آب", - "أيلول", - "تشرين الأول", - "تشرين الثاني", - "كانون الأول", - ] - - -class AlgeriaTunisiaArabicLocale(ArabicLocale): - names = ["ar_tn", "ar_dz"] - month_names = [ - "", - "جانفي", - "فيفري", - "مارس", - "أفريل", - "ماي", - "جوان", - "جويلية", - "أوت", - "سبتمبر", - "أكتوبر", - "نوفمبر", - "ديسمبر", - ] - month_abbreviations = [ - "", - "جانفي", - "فيفري", - "مارس", - "أفريل", - "ماي", - "جوان", - "جويلية", - "أوت", - "سبتمبر", - "أكتوبر", - "نوفمبر", - "ديسمبر", - ] - - -class MauritaniaArabicLocale(ArabicLocale): - names = ["ar_mr"] - month_names = [ - "", - "يناير", - "فبراير", - "مارس", - "إبريل", - "مايو", - "يونيو", - "يوليو", - "أغشت", - "شتمبر", - "أكتوبر", - "نوفمبر", - "دجمبر", - ] - month_abbreviations = [ - "", - "يناير", - "فبراير", - "مارس", - "إبريل", - "مايو", - "يونيو", - "يوليو", - "أغشت", - "شتمبر", - "أكتوبر", - "نوفمبر", - "دجمبر", - ] - - -class MoroccoArabicLocale(ArabicLocale): - names = ["ar_ma"] - month_names = [ - "", - "يناير", - "فبراير", - "مارس", - "أبريل", - "ماي", - "يونيو", - "يوليوز", - "غشت", - "شتنبر", - "أكتوبر", - "نونبر", - "دجنبر", - ] - month_abbreviations = [ - "", - "يناير", - "فبراير", - "مارس", - "أبريل", - "ماي", - "يونيو", - "يوليوز", - "غشت", - "شتنبر", - "أكتوبر", - "نونبر", - "دجنبر", - ] - - -class IcelandicLocale(Locale): - def _format_timeframe(self, timeframe, delta): - - timeframe = self.timeframes[timeframe] - if delta < 0: - timeframe = timeframe[0] - elif delta > 0: - timeframe = timeframe[1] - - return timeframe.format(abs(delta)) - - names = ["is", "is_is"] - - past = "fyrir {0} síðan" - future = "eftir {0}" - - timeframes = { - "now": "rétt í þessu", - "second": ("sekúndu", "sekúndu"), - "seconds": ("{0} nokkrum sekúndum", "nokkrar sekúndur"), - "minute": ("einni mínútu", "eina mínútu"), - "minutes": ("{0} mínútum", "{0} mínútur"), - "hour": ("einum tíma", "einn tíma"), - "hours": ("{0} tímum", "{0} tíma"), - "day": ("einum degi", "einn dag"), - "days": ("{0} dögum", "{0} daga"), - "month": ("einum mánuði", "einn mánuð"), - "months": ("{0} mánuðum", "{0} mánuði"), - "year": ("einu ári", "eitt ár"), - "years": ("{0} árum", "{0} ár"), - } - - meridians = {"am": "f.h.", "pm": "e.h.", "AM": "f.h.", "PM": "e.h."} - - month_names = [ - "", - "janúar", - "febrúar", - "mars", - "apríl", - "maí", - "júní", - "júlí", - "ágúst", - "september", - "október", - "nóvember", - "desember", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "maí", - "jún", - "júl", - "ágú", - "sep", - "okt", - "nóv", - "des", - ] - - day_names = [ - "", - "mánudagur", - "þriðjudagur", - "miðvikudagur", - "fimmtudagur", - "föstudagur", - "laugardagur", - "sunnudagur", - ] - day_abbreviations = ["", "mán", "þri", "mið", "fim", "fös", "lau", "sun"] - - -class DanishLocale(Locale): - - names = ["da", "da_dk"] - - past = "for {0} siden" - future = "efter {0}" - and_word = "og" - - timeframes = { - "now": "lige nu", - "second": "et sekund", - "seconds": "{0} et par sekunder", - "minute": "et minut", - "minutes": "{0} minutter", - "hour": "en time", - "hours": "{0} timer", - "day": "en dag", - "days": "{0} dage", - "month": "en måned", - "months": "{0} måneder", - "year": "et år", - "years": "{0} år", - } - - month_names = [ - "", - "januar", - "februar", - "marts", - "april", - "maj", - "juni", - "juli", - "august", - "september", - "oktober", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "maj", - "jun", - "jul", - "aug", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "mandag", - "tirsdag", - "onsdag", - "torsdag", - "fredag", - "lørdag", - "søndag", - ] - day_abbreviations = ["", "man", "tir", "ons", "tor", "fre", "lør", "søn"] - - -class MalayalamLocale(Locale): - - names = ["ml"] - - past = "{0} മുമ്പ്" - future = "{0} ശേഷം" - - timeframes = { - "now": "ഇപ്പോൾ", - "second": "ഒരു നിമിഷം", - "seconds": "{0} സെക്കന്റ്‌", - "minute": "ഒരു മിനിറ്റ്", - "minutes": "{0} മിനിറ്റ്", - "hour": "ഒരു മണിക്കൂർ", - "hours": "{0} മണിക്കൂർ", - "day": "ഒരു ദിവസം ", - "days": "{0} ദിവസം ", - "month": "ഒരു മാസം ", - "months": "{0} മാസം ", - "year": "ഒരു വർഷം ", - "years": "{0} വർഷം ", - } - - meridians = { - "am": "രാവിലെ", - "pm": "ഉച്ചക്ക് ശേഷം", - "AM": "രാവിലെ", - "PM": "ഉച്ചക്ക് ശേഷം", - } - - month_names = [ - "", - "ജനുവരി", - "ഫെബ്രുവരി", - "മാർച്ച്‌", - "ഏപ്രിൽ ", - "മെയ്‌ ", - "ജൂണ്‍", - "ജൂലൈ", - "ഓഗസ്റ്റ്‌", - "സെപ്റ്റംബർ", - "ഒക്ടോബർ", - "നവംബർ", - "ഡിസംബർ", - ] - month_abbreviations = [ - "", - "ജനു", - "ഫെബ് ", - "മാർ", - "ഏപ്രിൽ", - "മേയ്", - "ജൂണ്‍", - "ജൂലൈ", - "ഓഗസ്റ", - "സെപ്റ്റ", - "ഒക്ടോ", - "നവം", - "ഡിസം", - ] - - day_names = ["", "തിങ്കള്‍", "ചൊവ്വ", "ബുധന്‍", "വ്യാഴം", "വെള്ളി", "ശനി", "ഞായര്‍"] - day_abbreviations = [ - "", - "തിങ്കള്‍", - "ചൊവ്വ", - "ബുധന്‍", - "വ്യാഴം", - "വെള്ളി", - "ശനി", - "ഞായര്‍", - ] - - -class HindiLocale(Locale): - - names = ["hi"] - - past = "{0} पहले" - future = "{0} बाद" - - timeframes = { - "now": "अभी", - "second": "एक पल", - "seconds": "{0} सेकंड्", - "minute": "एक मिनट ", - "minutes": "{0} मिनट ", - "hour": "एक घंटा", - "hours": "{0} घंटे", - "day": "एक दिन", - "days": "{0} दिन", - "month": "एक माह ", - "months": "{0} महीने ", - "year": "एक वर्ष ", - "years": "{0} साल ", - } - - meridians = {"am": "सुबह", "pm": "शाम", "AM": "सुबह", "PM": "शाम"} - - month_names = [ - "", - "जनवरी", - "फरवरी", - "मार्च", - "अप्रैल ", - "मई", - "जून", - "जुलाई", - "अगस्त", - "सितंबर", - "अक्टूबर", - "नवंबर", - "दिसंबर", - ] - month_abbreviations = [ - "", - "जन", - "फ़र", - "मार्च", - "अप्रै", - "मई", - "जून", - "जुलाई", - "आग", - "सित", - "अकत", - "नवे", - "दिस", - ] - - day_names = [ - "", - "सोमवार", - "मंगलवार", - "बुधवार", - "गुरुवार", - "शुक्रवार", - "शनिवार", - "रविवार", - ] - day_abbreviations = ["", "सोम", "मंगल", "बुध", "गुरुवार", "शुक्र", "शनि", "रवि"] - - -class CzechLocale(Locale): - names = ["cs", "cs_cz"] - - timeframes = { - "now": "Teď", - "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"}, - "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekund"]}, - "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"}, - "minutes": {"past": "{0} minutami", "future": ["{0} minuty", "{0} minut"]}, - "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodin"}, - "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodin"]}, - "day": {"past": "dnem", "future": "den", "zero": "{0} dnů"}, - "days": {"past": "{0} dny", "future": ["{0} dny", "{0} dnů"]}, - "week": {"past": "týdnem", "future": "týden", "zero": "{0} týdnů"}, - "weeks": {"past": "{0} týdny", "future": ["{0} týdny", "{0} týdnů"]}, - "month": {"past": "měsícem", "future": "měsíc", "zero": "{0} měsíců"}, - "months": {"past": "{0} měsíci", "future": ["{0} měsíce", "{0} měsíců"]}, - "year": {"past": "rokem", "future": "rok", "zero": "{0} let"}, - "years": {"past": "{0} lety", "future": ["{0} roky", "{0} let"]}, - } - - past = "Před {0}" - future = "Za {0}" - - month_names = [ - "", - "leden", - "únor", - "březen", - "duben", - "květen", - "červen", - "červenec", - "srpen", - "září", - "říjen", - "listopad", - "prosinec", - ] - month_abbreviations = [ - "", - "led", - "úno", - "bře", - "dub", - "kvě", - "čvn", - "čvc", - "srp", - "zář", - "říj", - "lis", - "pro", - ] - - day_names = [ - "", - "pondělí", - "úterý", - "středa", - "čtvrtek", - "pátek", - "sobota", - "neděle", - ] - day_abbreviations = ["", "po", "út", "st", "čt", "pá", "so", "ne"] - - def _format_timeframe(self, timeframe, delta): - """Czech aware time frame format function, takes into account - the differences between past and future forms.""" - form = self.timeframes[timeframe] - if isinstance(form, dict): - if delta == 0: - form = form["zero"] # And *never* use 0 in the singular! - elif delta > 0: - form = form["future"] - else: - form = form["past"] - delta = abs(delta) - - if isinstance(form, list): - if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[0] - else: - form = form[1] - - return form.format(delta) - - -class SlovakLocale(Locale): - names = ["sk", "sk_sk"] - - timeframes = { - "now": "Teraz", - "second": {"past": "sekundou", "future": "sekundu", "zero": "{0} sekúnd"}, - "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekúnd"]}, - "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"}, - "minutes": {"past": "{0} minútami", "future": ["{0} minúty", "{0} minút"]}, - "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodín"}, - "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodín"]}, - "day": {"past": "dňom", "future": "deň", "zero": "{0} dní"}, - "days": {"past": "{0} dňami", "future": ["{0} dni", "{0} dní"]}, - "week": {"past": "týždňom", "future": "týždeň", "zero": "{0} týždňov"}, - "weeks": {"past": "{0} týždňami", "future": ["{0} týždne", "{0} týždňov"]}, - "month": {"past": "mesiacom", "future": "mesiac", "zero": "{0} mesiacov"}, - "months": {"past": "{0} mesiacmi", "future": ["{0} mesiace", "{0} mesiacov"]}, - "year": {"past": "rokom", "future": "rok", "zero": "{0} rokov"}, - "years": {"past": "{0} rokmi", "future": ["{0} roky", "{0} rokov"]}, - } - - past = "Pred {0}" - future = "O {0}" - and_word = "a" - - month_names = [ - "", - "január", - "február", - "marec", - "apríl", - "máj", - "jún", - "júl", - "august", - "september", - "október", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "máj", - "jún", - "júl", - "aug", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "pondelok", - "utorok", - "streda", - "štvrtok", - "piatok", - "sobota", - "nedeľa", - ] - day_abbreviations = ["", "po", "ut", "st", "št", "pi", "so", "ne"] - - def _format_timeframe(self, timeframe, delta): - """Slovak aware time frame format function, takes into account - the differences between past and future forms.""" - form = self.timeframes[timeframe] - if isinstance(form, dict): - if delta == 0: - form = form["zero"] # And *never* use 0 in the singular! - elif delta > 0: - form = form["future"] - else: - form = form["past"] - delta = abs(delta) - - if isinstance(form, list): - if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20): - form = form[0] - else: - form = form[1] - - return form.format(delta) - - -class FarsiLocale(Locale): - - names = ["fa", "fa_ir"] - - past = "{0} قبل" - future = "در {0}" - - timeframes = { - "now": "اکنون", - "second": "یک لحظه", - "seconds": "{0} ثانیه", - "minute": "یک دقیقه", - "minutes": "{0} دقیقه", - "hour": "یک ساعت", - "hours": "{0} ساعت", - "day": "یک روز", - "days": "{0} روز", - "month": "یک ماه", - "months": "{0} ماه", - "year": "یک سال", - "years": "{0} سال", - } - - meridians = { - "am": "قبل از ظهر", - "pm": "بعد از ظهر", - "AM": "قبل از ظهر", - "PM": "بعد از ظهر", - } - - month_names = [ - "", - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ] - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ] - - day_names = [ - "", - "دو شنبه", - "سه شنبه", - "چهارشنبه", - "پنجشنبه", - "جمعه", - "شنبه", - "یکشنبه", - ] - day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - - -class HebrewLocale(Locale): - - names = ["he", "he_IL"] - - past = "לפני {0}" - future = "בעוד {0}" - and_word = "ו" - - timeframes = { - "now": "הרגע", - "second": "שנייה", - "seconds": "{0} שניות", - "minute": "דקה", - "minutes": "{0} דקות", - "hour": "שעה", - "hours": "{0} שעות", - "2-hours": "שעתיים", - "day": "יום", - "days": "{0} ימים", - "2-days": "יומיים", - "week": "שבוע", - "weeks": "{0} שבועות", - "2-weeks": "שבועיים", - "month": "חודש", - "months": "{0} חודשים", - "2-months": "חודשיים", - "year": "שנה", - "years": "{0} שנים", - "2-years": "שנתיים", - } - - meridians = { - "am": 'לפנ"צ', - "pm": 'אחר"צ', - "AM": "לפני הצהריים", - "PM": "אחרי הצהריים", - } - - month_names = [ - "", - "ינואר", - "פברואר", - "מרץ", - "אפריל", - "מאי", - "יוני", - "יולי", - "אוגוסט", - "ספטמבר", - "אוקטובר", - "נובמבר", - "דצמבר", - ] - month_abbreviations = [ - "", - "ינו׳", - "פבר׳", - "מרץ", - "אפר׳", - "מאי", - "יוני", - "יולי", - "אוג׳", - "ספט׳", - "אוק׳", - "נוב׳", - "דצמ׳", - ] - - day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"] - day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"] - - def _format_timeframe(self, timeframe, delta): - """Hebrew couple of aware""" - couple = "2-{}".format(timeframe) - single = timeframe.rstrip("s") - if abs(delta) == 2 and couple in self.timeframes: - key = couple - elif abs(delta) == 1 and single in self.timeframes: - key = single - else: - key = timeframe - - return self.timeframes[key].format(trunc(abs(delta))) - - def describe_multi(self, timeframes, only_distance=False): - """Describes a delta within multiple timeframes in plain language. - In Hebrew, the and word behaves a bit differently. - - :param timeframes: a list of string, quantity pairs each representing a timeframe and delta. - :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords - """ - - humanized = "" - for index, (timeframe, delta) in enumerate(timeframes): - last_humanized = self._format_timeframe(timeframe, delta) - if index == 0: - humanized = last_humanized - elif index == len(timeframes) - 1: # Must have at least 2 items - humanized += " " + self.and_word - if last_humanized[0].isdecimal(): - humanized += "־" - humanized += last_humanized - else: # Don't add for the last one - humanized += ", " + last_humanized - - if not only_distance: - humanized = self._format_relative(humanized, timeframe, delta) - - return humanized - - -class MarathiLocale(Locale): - - names = ["mr"] - - past = "{0} आधी" - future = "{0} नंतर" - - timeframes = { - "now": "सद्य", - "second": "एक सेकंद", - "seconds": "{0} सेकंद", - "minute": "एक मिनिट ", - "minutes": "{0} मिनिट ", - "hour": "एक तास", - "hours": "{0} तास", - "day": "एक दिवस", - "days": "{0} दिवस", - "month": "एक महिना ", - "months": "{0} महिने ", - "year": "एक वर्ष ", - "years": "{0} वर्ष ", - } - - meridians = {"am": "सकाळ", "pm": "संध्याकाळ", "AM": "सकाळ", "PM": "संध्याकाळ"} - - month_names = [ - "", - "जानेवारी", - "फेब्रुवारी", - "मार्च", - "एप्रिल", - "मे", - "जून", - "जुलै", - "अॉगस्ट", - "सप्टेंबर", - "अॉक्टोबर", - "नोव्हेंबर", - "डिसेंबर", - ] - month_abbreviations = [ - "", - "जान", - "फेब्रु", - "मार्च", - "एप्रि", - "मे", - "जून", - "जुलै", - "अॉग", - "सप्टें", - "अॉक्टो", - "नोव्हें", - "डिसें", - ] - - day_names = [ - "", - "सोमवार", - "मंगळवार", - "बुधवार", - "गुरुवार", - "शुक्रवार", - "शनिवार", - "रविवार", - ] - day_abbreviations = ["", "सोम", "मंगळ", "बुध", "गुरु", "शुक्र", "शनि", "रवि"] - - -def _map_locales(): - - locales = {} - - for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): - if issubclass(cls, Locale): # pragma: no branch - for name in cls.names: - locales[name.lower()] = cls - - return locales - - -class CatalanLocale(Locale): - names = ["ca", "ca_es", "ca_ad", "ca_fr", "ca_it"] - past = "Fa {0}" - future = "En {0}" - and_word = "i" - - timeframes = { - "now": "Ara mateix", - "second": "un segon", - "seconds": "{0} segons", - "minute": "1 minut", - "minutes": "{0} minuts", - "hour": "una hora", - "hours": "{0} hores", - "day": "un dia", - "days": "{0} dies", - "month": "un mes", - "months": "{0} mesos", - "year": "un any", - "years": "{0} anys", - } - - month_names = [ - "", - "gener", - "febrer", - "març", - "abril", - "maig", - "juny", - "juliol", - "agost", - "setembre", - "octubre", - "novembre", - "desembre", - ] - month_abbreviations = [ - "", - "gen.", - "febr.", - "març", - "abr.", - "maig", - "juny", - "jul.", - "ag.", - "set.", - "oct.", - "nov.", - "des.", - ] - day_names = [ - "", - "dilluns", - "dimarts", - "dimecres", - "dijous", - "divendres", - "dissabte", - "diumenge", - ] - day_abbreviations = [ - "", - "dl.", - "dt.", - "dc.", - "dj.", - "dv.", - "ds.", - "dg.", - ] - - -class BasqueLocale(Locale): - names = ["eu", "eu_eu"] - past = "duela {0}" - future = "{0}" # I don't know what's the right phrase in Basque for the future. - - timeframes = { - "now": "Orain", - "second": "segundo bat", - "seconds": "{0} segundu", - "minute": "minutu bat", - "minutes": "{0} minutu", - "hour": "ordu bat", - "hours": "{0} ordu", - "day": "egun bat", - "days": "{0} egun", - "month": "hilabete bat", - "months": "{0} hilabet", - "year": "urte bat", - "years": "{0} urte", - } - - month_names = [ - "", - "urtarrilak", - "otsailak", - "martxoak", - "apirilak", - "maiatzak", - "ekainak", - "uztailak", - "abuztuak", - "irailak", - "urriak", - "azaroak", - "abenduak", - ] - month_abbreviations = [ - "", - "urt", - "ots", - "mar", - "api", - "mai", - "eka", - "uzt", - "abu", - "ira", - "urr", - "aza", - "abe", - ] - day_names = [ - "", - "astelehena", - "asteartea", - "asteazkena", - "osteguna", - "ostirala", - "larunbata", - "igandea", - ] - day_abbreviations = ["", "al", "ar", "az", "og", "ol", "lr", "ig"] - - -class HungarianLocale(Locale): - - names = ["hu", "hu_hu"] - - past = "{0} ezelőtt" - future = "{0} múlva" - - timeframes = { - "now": "éppen most", - "second": {"past": "egy második", "future": "egy második"}, - "seconds": {"past": "{0} másodpercekkel", "future": "{0} pár másodperc"}, - "minute": {"past": "egy perccel", "future": "egy perc"}, - "minutes": {"past": "{0} perccel", "future": "{0} perc"}, - "hour": {"past": "egy órával", "future": "egy óra"}, - "hours": {"past": "{0} órával", "future": "{0} óra"}, - "day": {"past": "egy nappal", "future": "egy nap"}, - "days": {"past": "{0} nappal", "future": "{0} nap"}, - "month": {"past": "egy hónappal", "future": "egy hónap"}, - "months": {"past": "{0} hónappal", "future": "{0} hónap"}, - "year": {"past": "egy évvel", "future": "egy év"}, - "years": {"past": "{0} évvel", "future": "{0} év"}, - } - - month_names = [ - "", - "január", - "február", - "március", - "április", - "május", - "június", - "július", - "augusztus", - "szeptember", - "október", - "november", - "december", - ] - month_abbreviations = [ - "", - "jan", - "febr", - "márc", - "ápr", - "máj", - "jún", - "júl", - "aug", - "szept", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "hétfő", - "kedd", - "szerda", - "csütörtök", - "péntek", - "szombat", - "vasárnap", - ] - day_abbreviations = ["", "hét", "kedd", "szer", "csüt", "pént", "szom", "vas"] - - meridians = {"am": "de", "pm": "du", "AM": "DE", "PM": "DU"} - - def _format_timeframe(self, timeframe, delta): - form = self.timeframes[timeframe] - - if isinstance(form, dict): - if delta > 0: - form = form["future"] - else: - form = form["past"] - - return form.format(abs(delta)) - - -class EsperantoLocale(Locale): - names = ["eo", "eo_xx"] - past = "antaŭ {0}" - future = "post {0}" - - timeframes = { - "now": "nun", - "second": "sekundo", - "seconds": "{0} kelkaj sekundoj", - "minute": "unu minuto", - "minutes": "{0} minutoj", - "hour": "un horo", - "hours": "{0} horoj", - "day": "unu tago", - "days": "{0} tagoj", - "month": "unu monato", - "months": "{0} monatoj", - "year": "unu jaro", - "years": "{0} jaroj", - } - - month_names = [ - "", - "januaro", - "februaro", - "marto", - "aprilo", - "majo", - "junio", - "julio", - "aŭgusto", - "septembro", - "oktobro", - "novembro", - "decembro", - ] - month_abbreviations = [ - "", - "jan", - "feb", - "mar", - "apr", - "maj", - "jun", - "jul", - "aŭg", - "sep", - "okt", - "nov", - "dec", - ] - - day_names = [ - "", - "lundo", - "mardo", - "merkredo", - "ĵaŭdo", - "vendredo", - "sabato", - "dimanĉo", - ] - day_abbreviations = ["", "lun", "mar", "mer", "ĵaŭ", "ven", "sab", "dim"] - - meridians = {"am": "atm", "pm": "ptm", "AM": "ATM", "PM": "PTM"} - - ordinal_day_re = r"((?P[1-3]?[0-9](?=a))a)" - - def _ordinal_number(self, n): - return "{}a".format(n) - - -class ThaiLocale(Locale): - - names = ["th", "th_th"] - - past = "{0}{1}ที่ผ่านมา" - future = "ในอีก{1}{0}" - - timeframes = { - "now": "ขณะนี้", - "second": "วินาที", - "seconds": "{0} ไม่กี่วินาที", - "minute": "1 นาที", - "minutes": "{0} นาที", - "hour": "1 ชั่วโมง", - "hours": "{0} ชั่วโมง", - "day": "1 วัน", - "days": "{0} วัน", - "month": "1 เดือน", - "months": "{0} เดือน", - "year": "1 ปี", - "years": "{0} ปี", - } - - month_names = [ - "", - "มกราคม", - "กุมภาพันธ์", - "มีนาคม", - "เมษายน", - "พฤษภาคม", - "มิถุนายน", - "กรกฎาคม", - "สิงหาคม", - "กันยายน", - "ตุลาคม", - "พฤศจิกายน", - "ธันวาคม", - ] - month_abbreviations = [ - "", - "ม.ค.", - "ก.พ.", - "มี.ค.", - "เม.ย.", - "พ.ค.", - "มิ.ย.", - "ก.ค.", - "ส.ค.", - "ก.ย.", - "ต.ค.", - "พ.ย.", - "ธ.ค.", - ] - - day_names = ["", "จันทร์", "อังคาร", "พุธ", "พฤหัสบดี", "ศุกร์", "เสาร์", "อาทิตย์"] - day_abbreviations = ["", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"] - - meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} - - BE_OFFSET = 543 - - def year_full(self, year): - """Thai always use Buddhist Era (BE) which is CE + 543""" - year += self.BE_OFFSET - return "{:04d}".format(year) - - def year_abbreviation(self, year): - """Thai always use Buddhist Era (BE) which is CE + 543""" - year += self.BE_OFFSET - return "{:04d}".format(year)[2:] - - def _format_relative(self, humanized, timeframe, delta): - """Thai normally doesn't have any space between words""" - if timeframe == "now": - return humanized - space = "" if timeframe == "seconds" else " " - direction = self.past if delta < 0 else self.future - - return direction.format(humanized, space) - - -class BengaliLocale(Locale): - - names = ["bn", "bn_bd", "bn_in"] - - past = "{0} আগে" - future = "{0} পরে" - - timeframes = { - "now": "এখন", - "second": "একটি দ্বিতীয়", - "seconds": "{0} সেকেন্ড", - "minute": "এক মিনিট", - "minutes": "{0} মিনিট", - "hour": "এক ঘণ্টা", - "hours": "{0} ঘণ্টা", - "day": "এক দিন", - "days": "{0} দিন", - "month": "এক মাস", - "months": "{0} মাস ", - "year": "এক বছর", - "years": "{0} বছর", - } - - meridians = {"am": "সকাল", "pm": "বিকাল", "AM": "সকাল", "PM": "বিকাল"} - - month_names = [ - "", - "জানুয়ারি", - "ফেব্রুয়ারি", - "মার্চ", - "এপ্রিল", - "মে", - "জুন", - "জুলাই", - "আগস্ট", - "সেপ্টেম্বর", - "অক্টোবর", - "নভেম্বর", - "ডিসেম্বর", - ] - month_abbreviations = [ - "", - "জানু", - "ফেব", - "মার্চ", - "এপ্রি", - "মে", - "জুন", - "জুল", - "অগা", - "সেপ্ট", - "অক্টো", - "নভে", - "ডিসে", - ] - - day_names = [ - "", - "সোমবার", - "মঙ্গলবার", - "বুধবার", - "বৃহস্পতিবার", - "শুক্রবার", - "শনিবার", - "রবিবার", - ] - day_abbreviations = ["", "সোম", "মঙ্গল", "বুধ", "বৃহঃ", "শুক্র", "শনি", "রবি"] - - def _ordinal_number(self, n): - if n > 10 or n == 0: - return "{}তম".format(n) - if n in [1, 5, 7, 8, 9, 10]: - return "{}ম".format(n) - if n in [2, 3]: - return "{}য়".format(n) - if n == 4: - return "{}র্থ".format(n) - if n == 6: - return "{}ষ্ঠ".format(n) - - -class RomanshLocale(Locale): - - names = ["rm", "rm_ch"] - - past = "avant {0}" - future = "en {0}" - - timeframes = { - "now": "en quest mument", - "second": "in secunda", - "seconds": "{0} secundas", - "minute": "ina minuta", - "minutes": "{0} minutas", - "hour": "in'ura", - "hours": "{0} ura", - "day": "in di", - "days": "{0} dis", - "month": "in mais", - "months": "{0} mais", - "year": "in onn", - "years": "{0} onns", - } - - month_names = [ - "", - "schaner", - "favrer", - "mars", - "avrigl", - "matg", - "zercladur", - "fanadur", - "avust", - "settember", - "october", - "november", - "december", - ] - - month_abbreviations = [ - "", - "schan", - "fav", - "mars", - "avr", - "matg", - "zer", - "fan", - "avu", - "set", - "oct", - "nov", - "dec", - ] - - day_names = [ - "", - "glindesdi", - "mardi", - "mesemna", - "gievgia", - "venderdi", - "sonda", - "dumengia", - ] - - day_abbreviations = ["", "gli", "ma", "me", "gie", "ve", "so", "du"] - - -class RomanianLocale(Locale): - names = ["ro", "ro_ro"] - - past = "{0} în urmă" - future = "peste {0}" - and_word = "și" - - timeframes = { - "now": "acum", - "second": "o secunda", - "seconds": "{0} câteva secunde", - "minute": "un minut", - "minutes": "{0} minute", - "hour": "o oră", - "hours": "{0} ore", - "day": "o zi", - "days": "{0} zile", - "month": "o lună", - "months": "{0} luni", - "year": "un an", - "years": "{0} ani", - } - - month_names = [ - "", - "ianuarie", - "februarie", - "martie", - "aprilie", - "mai", - "iunie", - "iulie", - "august", - "septembrie", - "octombrie", - "noiembrie", - "decembrie", - ] - month_abbreviations = [ - "", - "ian", - "febr", - "mart", - "apr", - "mai", - "iun", - "iul", - "aug", - "sept", - "oct", - "nov", - "dec", - ] - - day_names = [ - "", - "luni", - "marți", - "miercuri", - "joi", - "vineri", - "sâmbătă", - "duminică", - ] - day_abbreviations = ["", "Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"] - - -class SlovenianLocale(Locale): - names = ["sl", "sl_si"] - - past = "pred {0}" - future = "čez {0}" - and_word = "in" - - timeframes = { - "now": "zdaj", - "second": "sekundo", - "seconds": "{0} sekund", - "minute": "minuta", - "minutes": "{0} minutami", - "hour": "uro", - "hours": "{0} ur", - "day": "dan", - "days": "{0} dni", - "month": "mesec", - "months": "{0} mesecev", - "year": "leto", - "years": "{0} let", - } - - meridians = {"am": "", "pm": "", "AM": "", "PM": ""} - - month_names = [ - "", - "Januar", - "Februar", - "Marec", - "April", - "Maj", - "Junij", - "Julij", - "Avgust", - "September", - "Oktober", - "November", - "December", - ] - - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mar", - "Apr", - "Maj", - "Jun", - "Jul", - "Avg", - "Sep", - "Okt", - "Nov", - "Dec", - ] - - day_names = [ - "", - "Ponedeljek", - "Torek", - "Sreda", - "Četrtek", - "Petek", - "Sobota", - "Nedelja", - ] - - day_abbreviations = ["", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob", "Ned"] - - -class IndonesianLocale(Locale): - - names = ["id", "id_id"] - - past = "{0} yang lalu" - future = "dalam {0}" - and_word = "dan" - - timeframes = { - "now": "baru saja", - "second": "1 sebentar", - "seconds": "{0} detik", - "minute": "1 menit", - "minutes": "{0} menit", - "hour": "1 jam", - "hours": "{0} jam", - "day": "1 hari", - "days": "{0} hari", - "month": "1 bulan", - "months": "{0} bulan", - "year": "1 tahun", - "years": "{0} tahun", - } - - meridians = {"am": "", "pm": "", "AM": "", "PM": ""} - - month_names = [ - "", - "Januari", - "Februari", - "Maret", - "April", - "Mei", - "Juni", - "Juli", - "Agustus", - "September", - "Oktober", - "November", - "Desember", - ] - - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mar", - "Apr", - "Mei", - "Jun", - "Jul", - "Ags", - "Sept", - "Okt", - "Nov", - "Des", - ] - - day_names = ["", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"] - - day_abbreviations = [ - "", - "Senin", - "Selasa", - "Rabu", - "Kamis", - "Jumat", - "Sabtu", - "Minggu", - ] - - -class NepaliLocale(Locale): - names = ["ne", "ne_np"] - - past = "{0} पहिले" - future = "{0} पछी" - - timeframes = { - "now": "अहिले", - "second": "एक सेकेन्ड", - "seconds": "{0} सेकण्ड", - "minute": "मिनेट", - "minutes": "{0} मिनेट", - "hour": "एक घण्टा", - "hours": "{0} घण्टा", - "day": "एक दिन", - "days": "{0} दिन", - "month": "एक महिना", - "months": "{0} महिना", - "year": "एक बर्ष", - "years": "बर्ष", - } - - meridians = {"am": "पूर्वाह्न", "pm": "अपरान्ह", "AM": "पूर्वाह्न", "PM": "अपरान्ह"} - - month_names = [ - "", - "जनवरी", - "फेब्रुअरी", - "मार्च", - "एप्रील", - "मे", - "जुन", - "जुलाई", - "अगष्ट", - "सेप्टेम्बर", - "अक्टोबर", - "नोवेम्बर", - "डिसेम्बर", - ] - month_abbreviations = [ - "", - "जन", - "फेब", - "मार्च", - "एप्रील", - "मे", - "जुन", - "जुलाई", - "अग", - "सेप", - "अक्ट", - "नोव", - "डिस", - ] - - day_names = [ - "", - "सोमवार", - "मंगलवार", - "बुधवार", - "बिहिवार", - "शुक्रवार", - "शनिवार", - "आइतवार", - ] - - day_abbreviations = ["", "सोम", "मंगल", "बुध", "बिहि", "शुक्र", "शनि", "आइत"] - - -class EstonianLocale(Locale): - names = ["ee", "et"] - - past = "{0} tagasi" - future = "{0} pärast" - and_word = "ja" - - timeframes = { - "now": {"past": "just nüüd", "future": "just nüüd"}, - "second": {"past": "üks sekund", "future": "ühe sekundi"}, - "seconds": {"past": "{0} sekundit", "future": "{0} sekundi"}, - "minute": {"past": "üks minut", "future": "ühe minuti"}, - "minutes": {"past": "{0} minutit", "future": "{0} minuti"}, - "hour": {"past": "tund aega", "future": "tunni aja"}, - "hours": {"past": "{0} tundi", "future": "{0} tunni"}, - "day": {"past": "üks päev", "future": "ühe päeva"}, - "days": {"past": "{0} päeva", "future": "{0} päeva"}, - "month": {"past": "üks kuu", "future": "ühe kuu"}, - "months": {"past": "{0} kuud", "future": "{0} kuu"}, - "year": {"past": "üks aasta", "future": "ühe aasta"}, - "years": {"past": "{0} aastat", "future": "{0} aasta"}, - } - - month_names = [ - "", - "Jaanuar", - "Veebruar", - "Märts", - "Aprill", - "Mai", - "Juuni", - "Juuli", - "August", - "September", - "Oktoober", - "November", - "Detsember", - ] - month_abbreviations = [ - "", - "Jan", - "Veb", - "Mär", - "Apr", - "Mai", - "Jun", - "Jul", - "Aug", - "Sep", - "Okt", - "Nov", - "Dets", - ] - - day_names = [ - "", - "Esmaspäev", - "Teisipäev", - "Kolmapäev", - "Neljapäev", - "Reede", - "Laupäev", - "Pühapäev", - ] - day_abbreviations = ["", "Esm", "Teis", "Kolm", "Nelj", "Re", "Lau", "Püh"] - - def _format_timeframe(self, timeframe, delta): - form = self.timeframes[timeframe] - if delta > 0: - form = form["future"] - else: - form = form["past"] - return form.format(abs(delta)) - - -class SwahiliLocale(Locale): - - names = [ - "sw", - "sw_ke", - "sw_tz", - ] - - past = "{0} iliyopita" - future = "muda wa {0}" - and_word = "na" - - timeframes = { - "now": "sasa hivi", - "second": "sekunde", - "seconds": "sekunde {0}", - "minute": "dakika moja", - "minutes": "dakika {0}", - "hour": "saa moja", - "hours": "saa {0}", - "day": "siku moja", - "days": "siku {0}", - "week": "wiki moja", - "weeks": "wiki {0}", - "month": "mwezi moja", - "months": "miezi {0}", - "year": "mwaka moja", - "years": "miaka {0}", - } - - meridians = {"am": "asu", "pm": "mch", "AM": "ASU", "PM": "MCH"} - - month_names = [ - "", - "Januari", - "Februari", - "Machi", - "Aprili", - "Mei", - "Juni", - "Julai", - "Agosti", - "Septemba", - "Oktoba", - "Novemba", - "Desemba", - ] - month_abbreviations = [ - "", - "Jan", - "Feb", - "Mac", - "Apr", - "Mei", - "Jun", - "Jul", - "Ago", - "Sep", - "Okt", - "Nov", - "Des", - ] - - day_names = [ - "", - "Jumatatu", - "Jumanne", - "Jumatano", - "Alhamisi", - "Ijumaa", - "Jumamosi", - "Jumapili", - ] - day_abbreviations = [ - "", - "Jumatatu", - "Jumanne", - "Jumatano", - "Alhamisi", - "Ijumaa", - "Jumamosi", - "Jumapili", - ] - - -_locales = _map_locales() diff --git a/client/ayon_core/vendor/python/python_2/arrow/parser.py b/client/ayon_core/vendor/python/python_2/arrow/parser.py deleted file mode 100644 index 243fd1721c..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/parser.py +++ /dev/null @@ -1,596 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals - -import re -from datetime import datetime, timedelta - -from dateutil import tz - -from arrow import locales -from arrow.util import iso_to_gregorian, next_weekday, normalize_timestamp - -try: - from functools import lru_cache -except ImportError: # pragma: no cover - from backports.functools_lru_cache import lru_cache # pragma: no cover - - -class ParserError(ValueError): - pass - - -# Allows for ParserErrors to be propagated from _build_datetime() -# when day_of_year errors occur. -# Before this, the ParserErrors were caught by the try/except in -# _parse_multiformat() and the appropriate error message was not -# transmitted to the user. -class ParserMatchError(ParserError): - pass - - -class DateTimeParser(object): - - _FORMAT_RE = re.compile( - r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)" - ) - _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]") - - _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}") - _ONE_OR_TWO_OR_THREE_DIGIT_RE = re.compile(r"\d{1,3}") - _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+") - _TWO_DIGIT_RE = re.compile(r"\d{2}") - _THREE_DIGIT_RE = re.compile(r"\d{3}") - _FOUR_DIGIT_RE = re.compile(r"\d{4}") - _TZ_Z_RE = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z") - _TZ_ZZ_RE = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z") - _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+") - # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will - # break cases like "15 Jul 2000" and a format list (see issue #447) - _TIMESTAMP_RE = re.compile(r"^\-?\d+\.?\d+$") - _TIMESTAMP_EXPANDED_RE = re.compile(r"^\-?\d+$") - _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$") - _WEEK_DATE_RE = re.compile(r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?") - - _BASE_INPUT_RE_MAP = { - "YYYY": _FOUR_DIGIT_RE, - "YY": _TWO_DIGIT_RE, - "MM": _TWO_DIGIT_RE, - "M": _ONE_OR_TWO_DIGIT_RE, - "DDDD": _THREE_DIGIT_RE, - "DDD": _ONE_OR_TWO_OR_THREE_DIGIT_RE, - "DD": _TWO_DIGIT_RE, - "D": _ONE_OR_TWO_DIGIT_RE, - "HH": _TWO_DIGIT_RE, - "H": _ONE_OR_TWO_DIGIT_RE, - "hh": _TWO_DIGIT_RE, - "h": _ONE_OR_TWO_DIGIT_RE, - "mm": _TWO_DIGIT_RE, - "m": _ONE_OR_TWO_DIGIT_RE, - "ss": _TWO_DIGIT_RE, - "s": _ONE_OR_TWO_DIGIT_RE, - "X": _TIMESTAMP_RE, - "x": _TIMESTAMP_EXPANDED_RE, - "ZZZ": _TZ_NAME_RE, - "ZZ": _TZ_ZZ_RE, - "Z": _TZ_Z_RE, - "S": _ONE_OR_MORE_DIGIT_RE, - "W": _WEEK_DATE_RE, - } - - SEPARATORS = ["-", "/", "."] - - def __init__(self, locale="en_us", cache_size=0): - - self.locale = locales.get_locale(locale) - self._input_re_map = self._BASE_INPUT_RE_MAP.copy() - self._input_re_map.update( - { - "MMMM": self._generate_choice_re( - self.locale.month_names[1:], re.IGNORECASE - ), - "MMM": self._generate_choice_re( - self.locale.month_abbreviations[1:], re.IGNORECASE - ), - "Do": re.compile(self.locale.ordinal_day_re), - "dddd": self._generate_choice_re( - self.locale.day_names[1:], re.IGNORECASE - ), - "ddd": self._generate_choice_re( - self.locale.day_abbreviations[1:], re.IGNORECASE - ), - "d": re.compile(r"[1-7]"), - "a": self._generate_choice_re( - (self.locale.meridians["am"], self.locale.meridians["pm"]) - ), - # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to - # ensure backwards compatibility of this token - "A": self._generate_choice_re(self.locale.meridians.values()), - } - ) - if cache_size > 0: - self._generate_pattern_re = lru_cache(maxsize=cache_size)( - self._generate_pattern_re - ) - - # TODO: since we support more than ISO 8601, we should rename this function - # IDEA: break into multiple functions - def parse_iso(self, datetime_string, normalize_whitespace=False): - - if normalize_whitespace: - datetime_string = re.sub(r"\s+", " ", datetime_string.strip()) - - has_space_divider = " " in datetime_string - has_t_divider = "T" in datetime_string - - num_spaces = datetime_string.count(" ") - if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0: - raise ParserError( - "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format( - datetime_string - ) - ) - - has_time = has_space_divider or has_t_divider - has_tz = False - - # date formats (ISO 8601 and others) to test against - # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used) - formats = [ - "YYYY-MM-DD", - "YYYY-M-DD", - "YYYY-M-D", - "YYYY/MM/DD", - "YYYY/M/DD", - "YYYY/M/D", - "YYYY.MM.DD", - "YYYY.M.DD", - "YYYY.M.D", - "YYYYMMDD", - "YYYY-DDDD", - "YYYYDDDD", - "YYYY-MM", - "YYYY/MM", - "YYYY.MM", - "YYYY", - "W", - ] - - if has_time: - - if has_space_divider: - date_string, time_string = datetime_string.split(" ", 1) - else: - date_string, time_string = datetime_string.split("T", 1) - - time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE) - - time_components = self._TIME_RE.match(time_parts[0]) - - if time_components is None: - raise ParserError( - "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format." - ) - - ( - hours, - minutes, - seconds, - subseconds_sep, - subseconds, - ) = time_components.groups() - - has_tz = len(time_parts) == 2 - has_minutes = minutes is not None - has_seconds = seconds is not None - has_subseconds = subseconds is not None - - is_basic_time_format = ":" not in time_parts[0] - tz_format = "Z" - - # use 'ZZ' token instead since tz offset is present in non-basic format - if has_tz and ":" in time_parts[1]: - tz_format = "ZZ" - - time_sep = "" if is_basic_time_format else ":" - - if has_subseconds: - time_string = "HH{time_sep}mm{time_sep}ss{subseconds_sep}S".format( - time_sep=time_sep, subseconds_sep=subseconds_sep - ) - elif has_seconds: - time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep) - elif has_minutes: - time_string = "HH{time_sep}mm".format(time_sep=time_sep) - else: - time_string = "HH" - - if has_space_divider: - formats = ["{} {}".format(f, time_string) for f in formats] - else: - formats = ["{}T{}".format(f, time_string) for f in formats] - - if has_time and has_tz: - # Add "Z" or "ZZ" to the format strings to indicate to - # _parse_token() that a timezone needs to be parsed - formats = ["{}{}".format(f, tz_format) for f in formats] - - return self._parse_multiformat(datetime_string, formats) - - def parse(self, datetime_string, fmt, normalize_whitespace=False): - - if normalize_whitespace: - datetime_string = re.sub(r"\s+", " ", datetime_string) - - if isinstance(fmt, list): - return self._parse_multiformat(datetime_string, fmt) - - fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt) - - match = fmt_pattern_re.search(datetime_string) - - if match is None: - raise ParserMatchError( - "Failed to match '{}' when parsing '{}'".format(fmt, datetime_string) - ) - - parts = {} - for token in fmt_tokens: - if token == "Do": - value = match.group("value") - elif token == "W": - value = (match.group("year"), match.group("week"), match.group("day")) - else: - value = match.group(token) - self._parse_token(token, value, parts) - - return self._build_datetime(parts) - - def _generate_pattern_re(self, fmt): - - # fmt is a string of tokens like 'YYYY-MM-DD' - # we construct a new string by replacing each - # token by its pattern: - # 'YYYY-MM-DD' -> '(?P\d{4})-(?P\d{2})-(?P
\d{2})' - tokens = [] - offset = 0 - - # Escape all special RegEx chars - escaped_fmt = re.escape(fmt) - - # Extract the bracketed expressions to be reinserted later. - escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt) - - # Any number of S is the same as one. - # TODO: allow users to specify the number of digits to parse - escaped_fmt = re.sub(r"S+", "S", escaped_fmt) - - escaped_data = re.findall(self._ESCAPE_RE, fmt) - - fmt_pattern = escaped_fmt - - for m in self._FORMAT_RE.finditer(escaped_fmt): - token = m.group(0) - try: - input_re = self._input_re_map[token] - except KeyError: - raise ParserError("Unrecognized token '{}'".format(token)) - input_pattern = "(?P<{}>{})".format(token, input_re.pattern) - tokens.append(token) - # a pattern doesn't have the same length as the token - # it replaces! We keep the difference in the offset variable. - # This works because the string is scanned left-to-right and matches - # are returned in the order found by finditer. - fmt_pattern = ( - fmt_pattern[: m.start() + offset] - + input_pattern - + fmt_pattern[m.end() + offset :] - ) - offset += len(input_pattern) - (m.end() - m.start()) - - final_fmt_pattern = "" - split_fmt = fmt_pattern.split(r"\#") - - # Due to the way Python splits, 'split_fmt' will always be longer - for i in range(len(split_fmt)): - final_fmt_pattern += split_fmt[i] - if i < len(escaped_data): - final_fmt_pattern += escaped_data[i][1:-1] - - # Wrap final_fmt_pattern in a custom word boundary to strictly - # match the formatting pattern and filter out date and time formats - # that include junk such as: blah1998-09-12 blah, blah 1998-09-12blah, - # blah1998-09-12blah. The custom word boundary matches every character - # that is not a whitespace character to allow for searching for a date - # and time string in a natural language sentence. Therefore, searching - # for a string of the form YYYY-MM-DD in "blah 1998-09-12 blah" will - # work properly. - # Certain punctuation before or after the target pattern such as - # "1998-09-12," is permitted. For the full list of valid punctuation, - # see the documentation. - - starting_word_boundary = ( - r"(?\s])" # This is the list of punctuation that is ok before the pattern (i.e. "It can't not be these characters before the pattern") - r"(\b|^)" # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a negative number through i.e. before epoch numbers - ) - ending_word_boundary = ( - r"(?=[\,\.\;\:\?\!\"\'\`\[\]\{\}\(\)\<\>]?" # Positive lookahead stating that these punctuation marks can appear after the pattern at most 1 time - r"(?!\S))" # Don't allow any non-whitespace character after the punctuation - ) - bounded_fmt_pattern = r"{}{}{}".format( - starting_word_boundary, final_fmt_pattern, ending_word_boundary - ) - - return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE) - - def _parse_token(self, token, value, parts): - - if token == "YYYY": - parts["year"] = int(value) - - elif token == "YY": - value = int(value) - parts["year"] = 1900 + value if value > 68 else 2000 + value - - elif token in ["MMMM", "MMM"]: - parts["month"] = self.locale.month_number(value.lower()) - - elif token in ["MM", "M"]: - parts["month"] = int(value) - - elif token in ["DDDD", "DDD"]: - parts["day_of_year"] = int(value) - - elif token in ["DD", "D"]: - parts["day"] = int(value) - - elif token == "Do": - parts["day"] = int(value) - - elif token == "dddd": - # locale day names are 1-indexed - day_of_week = [x.lower() for x in self.locale.day_names].index( - value.lower() - ) - parts["day_of_week"] = day_of_week - 1 - - elif token == "ddd": - # locale day abbreviations are 1-indexed - day_of_week = [x.lower() for x in self.locale.day_abbreviations].index( - value.lower() - ) - parts["day_of_week"] = day_of_week - 1 - - elif token.upper() in ["HH", "H"]: - parts["hour"] = int(value) - - elif token in ["mm", "m"]: - parts["minute"] = int(value) - - elif token in ["ss", "s"]: - parts["second"] = int(value) - - elif token == "S": - # We have the *most significant* digits of an arbitrary-precision integer. - # We want the six most significant digits as an integer, rounded. - # IDEA: add nanosecond support somehow? Need datetime support for it first. - value = value.ljust(7, str("0")) - - # floating-point (IEEE-754) defaults to half-to-even rounding - seventh_digit = int(value[6]) - if seventh_digit == 5: - rounding = int(value[5]) % 2 - elif seventh_digit > 5: - rounding = 1 - else: - rounding = 0 - - parts["microsecond"] = int(value[:6]) + rounding - - elif token == "X": - parts["timestamp"] = float(value) - - elif token == "x": - parts["expanded_timestamp"] = int(value) - - elif token in ["ZZZ", "ZZ", "Z"]: - parts["tzinfo"] = TzinfoParser.parse(value) - - elif token in ["a", "A"]: - if value in (self.locale.meridians["am"], self.locale.meridians["AM"]): - parts["am_pm"] = "am" - elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]): - parts["am_pm"] = "pm" - - elif token == "W": - parts["weekdate"] = value - - @staticmethod - def _build_datetime(parts): - - weekdate = parts.get("weekdate") - - if weekdate is not None: - # we can use strptime (%G, %V, %u) in python 3.6 but these tokens aren't available before that - year, week = int(weekdate[0]), int(weekdate[1]) - - if weekdate[2] is not None: - day = int(weekdate[2]) - else: - # day not given, default to 1 - day = 1 - - dt = iso_to_gregorian(year, week, day) - parts["year"] = dt.year - parts["month"] = dt.month - parts["day"] = dt.day - - timestamp = parts.get("timestamp") - - if timestamp is not None: - return datetime.fromtimestamp(timestamp, tz=tz.tzutc()) - - expanded_timestamp = parts.get("expanded_timestamp") - - if expanded_timestamp is not None: - return datetime.fromtimestamp( - normalize_timestamp(expanded_timestamp), - tz=tz.tzutc(), - ) - - day_of_year = parts.get("day_of_year") - - if day_of_year is not None: - year = parts.get("year") - month = parts.get("month") - if year is None: - raise ParserError( - "Year component is required with the DDD and DDDD tokens." - ) - - if month is not None: - raise ParserError( - "Month component is not allowed with the DDD and DDDD tokens." - ) - - date_string = "{}-{}".format(year, day_of_year) - try: - dt = datetime.strptime(date_string, "%Y-%j") - except ValueError: - raise ParserError( - "The provided day of year '{}' is invalid.".format(day_of_year) - ) - - parts["year"] = dt.year - parts["month"] = dt.month - parts["day"] = dt.day - - day_of_week = parts.get("day_of_week") - day = parts.get("day") - - # If day is passed, ignore day of week - if day_of_week is not None and day is None: - year = parts.get("year", 1970) - month = parts.get("month", 1) - day = 1 - - # dddd => first day of week after epoch - # dddd YYYY => first day of week in specified year - # dddd MM YYYY => first day of week in specified year and month - # dddd MM => first day after epoch in specified month - next_weekday_dt = next_weekday(datetime(year, month, day), day_of_week) - parts["year"] = next_weekday_dt.year - parts["month"] = next_weekday_dt.month - parts["day"] = next_weekday_dt.day - - am_pm = parts.get("am_pm") - hour = parts.get("hour", 0) - - if am_pm == "pm" and hour < 12: - hour += 12 - elif am_pm == "am" and hour == 12: - hour = 0 - - # Support for midnight at the end of day - if hour == 24: - if parts.get("minute", 0) != 0: - raise ParserError("Midnight at the end of day must not contain minutes") - if parts.get("second", 0) != 0: - raise ParserError("Midnight at the end of day must not contain seconds") - if parts.get("microsecond", 0) != 0: - raise ParserError( - "Midnight at the end of day must not contain microseconds" - ) - hour = 0 - day_increment = 1 - else: - day_increment = 0 - - # account for rounding up to 1000000 - microsecond = parts.get("microsecond", 0) - if microsecond == 1000000: - microsecond = 0 - second_increment = 1 - else: - second_increment = 0 - - increment = timedelta(days=day_increment, seconds=second_increment) - - return ( - datetime( - year=parts.get("year", 1), - month=parts.get("month", 1), - day=parts.get("day", 1), - hour=hour, - minute=parts.get("minute", 0), - second=parts.get("second", 0), - microsecond=microsecond, - tzinfo=parts.get("tzinfo"), - ) - + increment - ) - - def _parse_multiformat(self, string, formats): - - _datetime = None - - for fmt in formats: - try: - _datetime = self.parse(string, fmt) - break - except ParserMatchError: - pass - - if _datetime is None: - raise ParserError( - "Could not match input '{}' to any of the following formats: {}".format( - string, ", ".join(formats) - ) - ) - - return _datetime - - # generates a capture group of choices separated by an OR operator - @staticmethod - def _generate_choice_re(choices, flags=0): - return re.compile(r"({})".format("|".join(choices)), flags=flags) - - -class TzinfoParser(object): - _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$") - - @classmethod - def parse(cls, tzinfo_string): - - tzinfo = None - - if tzinfo_string == "local": - tzinfo = tz.tzlocal() - - elif tzinfo_string in ["utc", "UTC", "Z"]: - tzinfo = tz.tzutc() - - else: - - iso_match = cls._TZINFO_RE.match(tzinfo_string) - - if iso_match: - sign, hours, minutes = iso_match.groups() - if minutes is None: - minutes = 0 - seconds = int(hours) * 3600 + int(minutes) * 60 - - if sign == "-": - seconds *= -1 - - tzinfo = tz.tzoffset(None, seconds) - - else: - tzinfo = tz.gettz(tzinfo_string) - - if tzinfo is None: - raise ParserError( - 'Could not parse timezone expression "{}"'.format(tzinfo_string) - ) - - return tzinfo diff --git a/client/ayon_core/vendor/python/python_2/arrow/util.py b/client/ayon_core/vendor/python/python_2/arrow/util.py deleted file mode 100644 index acce8878df..0000000000 --- a/client/ayon_core/vendor/python/python_2/arrow/util.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -import datetime -import numbers - -from dateutil.rrule import WEEKLY, rrule - -from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US - - -def next_weekday(start_date, weekday): - """Get next weekday from the specified start date. - - :param start_date: Datetime object representing the start date. - :param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday). - :return: Datetime object corresponding to the next weekday after start_date. - - Usage:: - - # Get first Monday after epoch - >>> next_weekday(datetime(1970, 1, 1), 0) - 1970-01-05 00:00:00 - - # Get first Thursday after epoch - >>> next_weekday(datetime(1970, 1, 1), 3) - 1970-01-01 00:00:00 - - # Get first Sunday after epoch - >>> next_weekday(datetime(1970, 1, 1), 6) - 1970-01-04 00:00:00 - """ - if weekday < 0 or weekday > 6: - raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).") - return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0] - - -def total_seconds(td): - """Get total seconds for timedelta.""" - return td.total_seconds() - - -def is_timestamp(value): - """Check if value is a valid timestamp.""" - if isinstance(value, bool): - return False - if not ( - isinstance(value, numbers.Integral) - or isinstance(value, float) - or isinstance(value, str) - ): - return False - try: - float(value) - return True - except ValueError: - return False - - -def normalize_timestamp(timestamp): - """Normalize millisecond and microsecond timestamps into normal timestamps.""" - if timestamp > MAX_TIMESTAMP: - if timestamp < MAX_TIMESTAMP_MS: - timestamp /= 1e3 - elif timestamp < MAX_TIMESTAMP_US: - timestamp /= 1e6 - else: - raise ValueError( - "The specified timestamp '{}' is too large.".format(timestamp) - ) - return timestamp - - -# Credit to https://stackoverflow.com/a/1700069 -def iso_to_gregorian(iso_year, iso_week, iso_day): - """Converts an ISO week date tuple into a datetime object.""" - - if not 1 <= iso_week <= 53: - raise ValueError("ISO Calendar week value must be between 1-53.") - - if not 1 <= iso_day <= 7: - raise ValueError("ISO Calendar day value must be between 1-7") - - # The first week of the year always contains 4 Jan. - fourth_jan = datetime.date(iso_year, 1, 4) - delta = datetime.timedelta(fourth_jan.isoweekday() - 1) - year_start = fourth_jan - delta - gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1) - - return gregorian - - -def validate_bounds(bounds): - if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": - raise ValueError( - 'Invalid bounds. Please select between "()", "(]", "[)", or "[]".' - ) - - -# Python 2.7 / 3.0+ definitions for isstr function. - -try: # pragma: no cover - basestring - - def isstr(s): - return isinstance(s, basestring) # noqa: F821 - - -except NameError: # pragma: no cover - - def isstr(s): - return isinstance(s, str) - - -__all__ = ["next_weekday", "total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"] diff --git a/client/ayon_core/vendor/python/python_2/attr/__init__.py b/client/ayon_core/vendor/python/python_2/attr/__init__.py deleted file mode 100644 index f95c96dd57..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import sys - -from functools import partial - -from . import converters, exceptions, filters, setters, validators -from ._cmp import cmp_using -from ._config import get_run_validators, set_run_validators -from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types -from ._make import ( - NOTHING, - Attribute, - Factory, - attrib, - attrs, - fields, - fields_dict, - make_class, - validate, -) -from ._version_info import VersionInfo - - -__version__ = "21.4.0" -__version_info__ = VersionInfo._from_version_string(__version__) - -__title__ = "attrs" -__description__ = "Classes Without Boilerplate" -__url__ = "https://www.attrs.org/" -__uri__ = __url__ -__doc__ = __description__ + " <" + __uri__ + ">" - -__author__ = "Hynek Schlawack" -__email__ = "hs@ox.cx" - -__license__ = "MIT" -__copyright__ = "Copyright (c) 2015 Hynek Schlawack" - - -s = attributes = attrs -ib = attr = attrib -dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) - -__all__ = [ - "Attribute", - "Factory", - "NOTHING", - "asdict", - "assoc", - "astuple", - "attr", - "attrib", - "attributes", - "attrs", - "cmp_using", - "converters", - "evolve", - "exceptions", - "fields", - "fields_dict", - "filters", - "get_run_validators", - "has", - "ib", - "make_class", - "resolve_types", - "s", - "set_run_validators", - "setters", - "validate", - "validators", -] - -if sys.version_info[:2] >= (3, 6): - from ._next_gen import define, field, frozen, mutable # noqa: F401 - - __all__.extend(("define", "field", "frozen", "mutable")) diff --git a/client/ayon_core/vendor/python/python_2/attr/__init__.pyi b/client/ayon_core/vendor/python/python_2/attr/__init__.pyi deleted file mode 100644 index c0a2126503..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/__init__.pyi +++ /dev/null @@ -1,484 +0,0 @@ -import sys - -from typing import ( - Any, - Callable, - Dict, - Generic, - List, - Mapping, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, - overload, -) - -# `import X as X` is required to make these public -from . import converters as converters -from . import exceptions as exceptions -from . import filters as filters -from . import setters as setters -from . import validators as validators -from ._version_info import VersionInfo - -__version__: str -__version_info__: VersionInfo -__title__: str -__description__: str -__url__: str -__uri__: str -__author__: str -__email__: str -__license__: str -__copyright__: str - -_T = TypeVar("_T") -_C = TypeVar("_C", bound=type) - -_EqOrderType = Union[bool, Callable[[Any], Any]] -_ValidatorType = Callable[[Any, Attribute[_T], _T], Any] -_ConverterType = Callable[[Any], Any] -_FilterType = Callable[[Attribute[_T], _T], bool] -_ReprType = Callable[[Any], str] -_ReprArgType = Union[bool, _ReprType] -_OnSetAttrType = Callable[[Any, Attribute[Any], Any], Any] -_OnSetAttrArgType = Union[ - _OnSetAttrType, List[_OnSetAttrType], setters._NoOpType -] -_FieldTransformer = Callable[ - [type, List[Attribute[Any]]], List[Attribute[Any]] -] -_CompareWithType = Callable[[Any, Any], bool] -# FIXME: in reality, if multiple validators are passed they must be in a list -# or tuple, but those are invariant and so would prevent subtypes of -# _ValidatorType from working when passed in a list or tuple. -_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] - -# _make -- - -NOTHING: object - -# NOTE: Factory lies about its return type to make this possible: -# `x: List[int] # = Factory(list)` -# Work around mypy issue #4554 in the common case by using an overload. -if sys.version_info >= (3, 8): - from typing import Literal - @overload - def Factory(factory: Callable[[], _T]) -> _T: ... - @overload - def Factory( - factory: Callable[[Any], _T], - takes_self: Literal[True], - ) -> _T: ... - @overload - def Factory( - factory: Callable[[], _T], - takes_self: Literal[False], - ) -> _T: ... - -else: - @overload - def Factory(factory: Callable[[], _T]) -> _T: ... - @overload - def Factory( - factory: Union[Callable[[Any], _T], Callable[[], _T]], - takes_self: bool = ..., - ) -> _T: ... - -# Static type inference support via __dataclass_transform__ implemented as per: -# https://github.com/microsoft/pyright/blob/1.1.135/specs/dataclass_transforms.md -# This annotation must be applied to all overloads of "define" and "attrs" -# -# NOTE: This is a typing construct and does not exist at runtime. Extensions -# wrapping attrs decorators should declare a separate __dataclass_transform__ -# signature in the extension module using the specification linked above to -# provide pyright support. -def __dataclass_transform__( - *, - eq_default: bool = True, - order_default: bool = False, - kw_only_default: bool = False, - field_descriptors: Tuple[Union[type, Callable[..., Any]], ...] = (()), -) -> Callable[[_T], _T]: ... - -class Attribute(Generic[_T]): - name: str - default: Optional[_T] - validator: Optional[_ValidatorType[_T]] - repr: _ReprArgType - cmp: _EqOrderType - eq: _EqOrderType - order: _EqOrderType - hash: Optional[bool] - init: bool - converter: Optional[_ConverterType] - metadata: Dict[Any, Any] - type: Optional[Type[_T]] - kw_only: bool - on_setattr: _OnSetAttrType - def evolve(self, **changes: Any) -> "Attribute[Any]": ... - -# NOTE: We had several choices for the annotation to use for type arg: -# 1) Type[_T] -# - Pros: Handles simple cases correctly -# - Cons: Might produce less informative errors in the case of conflicting -# TypeVars e.g. `attr.ib(default='bad', type=int)` -# 2) Callable[..., _T] -# - Pros: Better error messages than #1 for conflicting TypeVars -# - Cons: Terrible error messages for validator checks. -# e.g. attr.ib(type=int, validator=validate_str) -# -> error: Cannot infer function type argument -# 3) type (and do all of the work in the mypy plugin) -# - Pros: Simple here, and we could customize the plugin with our own errors. -# - Cons: Would need to write mypy plugin code to handle all the cases. -# We chose option #1. - -# `attr` lies about its return type to make the following possible: -# attr() -> Any -# attr(8) -> int -# attr(validator=) -> Whatever the callable expects. -# This makes this type of assignments possible: -# x: int = attr(8) -# -# This form catches explicit None or no default but with no other arguments -# returns Any. -@overload -def attrib( - default: None = ..., - validator: None = ..., - repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: None = ..., - converter: None = ..., - factory: None = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> Any: ... - -# This form catches an explicit None or no default and infers the type from the -# other arguments. -@overload -def attrib( - default: None = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> _T: ... - -# This form catches an explicit default argument. -@overload -def attrib( - default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> _T: ... - -# This form covers type=non-Type: e.g. forward references (str), Any -@overload -def attrib( - default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: object = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> Any: ... -@overload -def field( - *, - default: None = ..., - validator: None = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: None = ..., - factory: None = ..., - kw_only: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> Any: ... - -# This form catches an explicit None or no default and infers the type from the -# other arguments. -@overload -def field( - *, - default: None = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> _T: ... - -# This form catches an explicit default argument. -@overload -def field( - *, - default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> _T: ... - -# This form covers type=non-Type: e.g. forward references (str), Any -@overload -def field( - *, - default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: _ReprArgType = ..., - hash: Optional[bool] = ..., - init: bool = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - converter: Optional[_ConverterType] = ..., - factory: Optional[Callable[[], _T]] = ..., - kw_only: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., -) -> Any: ... -@overload -@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) -def attrs( - maybe_cls: _C, - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - auto_detect: bool = ..., - collect_by_mro: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> _C: ... -@overload -@__dataclass_transform__(order_default=True, field_descriptors=(attrib, field)) -def attrs( - maybe_cls: None = ..., - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - auto_detect: bool = ..., - collect_by_mro: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> Callable[[_C], _C]: ... -@overload -@__dataclass_transform__(field_descriptors=(attrib, field)) -def define( - maybe_cls: _C, - *, - these: Optional[Dict[str, Any]] = ..., - repr: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - auto_detect: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> _C: ... -@overload -@__dataclass_transform__(field_descriptors=(attrib, field)) -def define( - maybe_cls: None = ..., - *, - these: Optional[Dict[str, Any]] = ..., - repr: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[bool] = ..., - order: Optional[bool] = ..., - auto_detect: bool = ..., - getstate_setstate: Optional[bool] = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., - match_args: bool = ..., -) -> Callable[[_C], _C]: ... - -mutable = define -frozen = define # they differ only in their defaults - -# TODO: add support for returning NamedTuple from the mypy plugin -class _Fields(Tuple[Attribute[Any], ...]): - def __getattr__(self, name: str) -> Attribute[Any]: ... - -def fields(cls: type) -> _Fields: ... -def fields_dict(cls: type) -> Dict[str, Attribute[Any]]: ... -def validate(inst: Any) -> None: ... -def resolve_types( - cls: _C, - globalns: Optional[Dict[str, Any]] = ..., - localns: Optional[Dict[str, Any]] = ..., - attribs: Optional[List[Attribute[Any]]] = ..., -) -> _C: ... - -# TODO: add support for returning a proper attrs class from the mypy plugin -# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', -# [attr.ib()])` is valid -def make_class( - name: str, - attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], - bases: Tuple[type, ...] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: Optional[_EqOrderType] = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - weakref_slot: bool = ..., - str: bool = ..., - auto_attribs: bool = ..., - kw_only: bool = ..., - cache_hash: bool = ..., - auto_exc: bool = ..., - eq: Optional[_EqOrderType] = ..., - order: Optional[_EqOrderType] = ..., - collect_by_mro: bool = ..., - on_setattr: Optional[_OnSetAttrArgType] = ..., - field_transformer: Optional[_FieldTransformer] = ..., -) -> type: ... - -# _funcs -- - -# TODO: add support for returning TypedDict from the mypy plugin -# FIXME: asdict/astuple do not honor their factory args. Waiting on one of -# these: -# https://github.com/python/mypy/issues/4236 -# https://github.com/python/typing/issues/253 -# XXX: remember to fix attrs.asdict/astuple too! -def asdict( - inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - dict_factory: Type[Mapping[Any, Any]] = ..., - retain_collection_types: bool = ..., - value_serializer: Optional[ - Callable[[type, Attribute[Any], Any], Any] - ] = ..., - tuple_keys: Optional[bool] = ..., -) -> Dict[str, Any]: ... - -# TODO: add support for returning NamedTuple from the mypy plugin -def astuple( - inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - tuple_factory: Type[Sequence[Any]] = ..., - retain_collection_types: bool = ..., -) -> Tuple[Any, ...]: ... -def has(cls: type) -> bool: ... -def assoc(inst: _T, **changes: Any) -> _T: ... -def evolve(inst: _T, **changes: Any) -> _T: ... - -# _config -- - -def set_run_validators(run: bool) -> None: ... -def get_run_validators() -> bool: ... - -# aliases -- - -s = attributes = attrs -ib = attr = attrib -dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/client/ayon_core/vendor/python/python_2/attr/_cmp.py b/client/ayon_core/vendor/python/python_2/attr/_cmp.py deleted file mode 100644 index 6cffa4dbab..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_cmp.py +++ /dev/null @@ -1,154 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import functools - -from ._compat import new_class -from ._make import _make_ne - - -_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} - - -def cmp_using( - eq=None, - lt=None, - le=None, - gt=None, - ge=None, - require_same_type=True, - class_name="Comparable", -): - """ - Create a class that can be passed into `attr.ib`'s ``eq``, ``order``, and - ``cmp`` arguments to customize field comparison. - - The resulting class will have a full set of ordering methods if - at least one of ``{lt, le, gt, ge}`` and ``eq`` are provided. - - :param Optional[callable] eq: `callable` used to evaluate equality - of two objects. - :param Optional[callable] lt: `callable` used to evaluate whether - one object is less than another object. - :param Optional[callable] le: `callable` used to evaluate whether - one object is less than or equal to another object. - :param Optional[callable] gt: `callable` used to evaluate whether - one object is greater than another object. - :param Optional[callable] ge: `callable` used to evaluate whether - one object is greater than or equal to another object. - - :param bool require_same_type: When `True`, equality and ordering methods - will return `NotImplemented` if objects are not of the same type. - - :param Optional[str] class_name: Name of class. Defaults to 'Comparable'. - - See `comparison` for more details. - - .. versionadded:: 21.1.0 - """ - - body = { - "__slots__": ["value"], - "__init__": _make_init(), - "_requirements": [], - "_is_comparable_to": _is_comparable_to, - } - - # Add operations. - num_order_functions = 0 - has_eq_function = False - - if eq is not None: - has_eq_function = True - body["__eq__"] = _make_operator("eq", eq) - body["__ne__"] = _make_ne() - - if lt is not None: - num_order_functions += 1 - body["__lt__"] = _make_operator("lt", lt) - - if le is not None: - num_order_functions += 1 - body["__le__"] = _make_operator("le", le) - - if gt is not None: - num_order_functions += 1 - body["__gt__"] = _make_operator("gt", gt) - - if ge is not None: - num_order_functions += 1 - body["__ge__"] = _make_operator("ge", ge) - - type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body)) - - # Add same type requirement. - if require_same_type: - type_._requirements.append(_check_same_type) - - # Add total ordering if at least one operation was defined. - if 0 < num_order_functions < 4: - if not has_eq_function: - # functools.total_ordering requires __eq__ to be defined, - # so raise early error here to keep a nice stack. - raise ValueError( - "eq must be define is order to complete ordering from " - "lt, le, gt, ge." - ) - type_ = functools.total_ordering(type_) - - return type_ - - -def _make_init(): - """ - Create __init__ method. - """ - - def __init__(self, value): - """ - Initialize object with *value*. - """ - self.value = value - - return __init__ - - -def _make_operator(name, func): - """ - Create operator method. - """ - - def method(self, other): - if not self._is_comparable_to(other): - return NotImplemented - - result = func(self.value, other.value) - if result is NotImplemented: - return NotImplemented - - return result - - method.__name__ = "__%s__" % (name,) - method.__doc__ = "Return a %s b. Computed by attrs." % ( - _operation_names[name], - ) - - return method - - -def _is_comparable_to(self, other): - """ - Check whether `other` is comparable to `self`. - """ - for func in self._requirements: - if not func(self, other): - return False - return True - - -def _check_same_type(self, other): - """ - Return True if *self* and *other* are of the same type, False otherwise. - """ - return other.value.__class__ is self.value.__class__ diff --git a/client/ayon_core/vendor/python/python_2/attr/_cmp.pyi b/client/ayon_core/vendor/python/python_2/attr/_cmp.pyi deleted file mode 100644 index e71aaff7a1..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_cmp.pyi +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Type - -from . import _CompareWithType - -def cmp_using( - eq: Optional[_CompareWithType], - lt: Optional[_CompareWithType], - le: Optional[_CompareWithType], - gt: Optional[_CompareWithType], - ge: Optional[_CompareWithType], - require_same_type: bool, - class_name: str, -) -> Type: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/_compat.py b/client/ayon_core/vendor/python/python_2/attr/_compat.py deleted file mode 100644 index dc0cb02b64..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_compat.py +++ /dev/null @@ -1,261 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import platform -import sys -import threading -import types -import warnings - - -PY2 = sys.version_info[0] == 2 -PYPY = platform.python_implementation() == "PyPy" -PY36 = sys.version_info[:2] >= (3, 6) -HAS_F_STRINGS = PY36 -PY310 = sys.version_info[:2] >= (3, 10) - - -if PYPY or PY36: - ordered_dict = dict -else: - from collections import OrderedDict - - ordered_dict = OrderedDict - - -if PY2: - from collections import Mapping, Sequence - - from UserDict import IterableUserDict - - # We 'bundle' isclass instead of using inspect as importing inspect is - # fairly expensive (order of 10-15 ms for a modern machine in 2016) - def isclass(klass): - return isinstance(klass, (type, types.ClassType)) - - def new_class(name, bases, kwds, exec_body): - """ - A minimal stub of types.new_class that we need for make_class. - """ - ns = {} - exec_body(ns) - - return type(name, bases, ns) - - # TYPE is used in exceptions, repr(int) is different on Python 2 and 3. - TYPE = "type" - - def iteritems(d): - return d.iteritems() - - # Python 2 is bereft of a read-only dict proxy, so we make one! - class ReadOnlyDict(IterableUserDict): - """ - Best-effort read-only dict wrapper. - """ - - def __setitem__(self, key, val): - # We gently pretend we're a Python 3 mappingproxy. - raise TypeError( - "'mappingproxy' object does not support item assignment" - ) - - def update(self, _): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'update'" - ) - - def __delitem__(self, _): - # We gently pretend we're a Python 3 mappingproxy. - raise TypeError( - "'mappingproxy' object does not support item deletion" - ) - - def clear(self): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'clear'" - ) - - def pop(self, key, default=None): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'pop'" - ) - - def popitem(self): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'popitem'" - ) - - def setdefault(self, key, default=None): - # We gently pretend we're a Python 3 mappingproxy. - raise AttributeError( - "'mappingproxy' object has no attribute 'setdefault'" - ) - - def __repr__(self): - # Override to be identical to the Python 3 version. - return "mappingproxy(" + repr(self.data) + ")" - - def metadata_proxy(d): - res = ReadOnlyDict() - res.data.update(d) # We blocked update, so we have to do it like this. - return res - - def just_warn(*args, **kw): # pragma: no cover - """ - We only warn on Python 3 because we are not aware of any concrete - consequences of not setting the cell on Python 2. - """ - -else: # Python 3 and later. - from collections.abc import Mapping, Sequence # noqa - - def just_warn(*args, **kw): - """ - We only warn on Python 3 because we are not aware of any concrete - consequences of not setting the cell on Python 2. - """ - warnings.warn( - "Running interpreter doesn't sufficiently support code object " - "introspection. Some features like bare super() or accessing " - "__class__ will not work with slotted classes.", - RuntimeWarning, - stacklevel=2, - ) - - def isclass(klass): - return isinstance(klass, type) - - TYPE = "class" - - def iteritems(d): - return d.items() - - new_class = types.new_class - - def metadata_proxy(d): - return types.MappingProxyType(dict(d)) - - -def make_set_closure_cell(): - """Return a function of two arguments (cell, value) which sets - the value stored in the closure cell `cell` to `value`. - """ - # pypy makes this easy. (It also supports the logic below, but - # why not do the easy/fast thing?) - if PYPY: - - def set_closure_cell(cell, value): - cell.__setstate__((value,)) - - return set_closure_cell - - # Otherwise gotta do it the hard way. - - # Create a function that will set its first cellvar to `value`. - def set_first_cellvar_to(value): - x = value - return - - # This function will be eliminated as dead code, but - # not before its reference to `x` forces `x` to be - # represented as a closure cell rather than a local. - def force_x_to_be_a_cell(): # pragma: no cover - return x - - try: - # Extract the code object and make sure our assumptions about - # the closure behavior are correct. - if PY2: - co = set_first_cellvar_to.func_code - else: - co = set_first_cellvar_to.__code__ - if co.co_cellvars != ("x",) or co.co_freevars != (): - raise AssertionError # pragma: no cover - - # Convert this code object to a code object that sets the - # function's first _freevar_ (not cellvar) to the argument. - if sys.version_info >= (3, 8): - # CPython 3.8+ has an incompatible CodeType signature - # (added a posonlyargcount argument) but also added - # CodeType.replace() to do this without counting parameters. - set_first_freevar_code = co.replace( - co_cellvars=co.co_freevars, co_freevars=co.co_cellvars - ) - else: - args = [co.co_argcount] - if not PY2: - args.append(co.co_kwonlyargcount) - args.extend( - [ - co.co_nlocals, - co.co_stacksize, - co.co_flags, - co.co_code, - co.co_consts, - co.co_names, - co.co_varnames, - co.co_filename, - co.co_name, - co.co_firstlineno, - co.co_lnotab, - # These two arguments are reversed: - co.co_cellvars, - co.co_freevars, - ] - ) - set_first_freevar_code = types.CodeType(*args) - - def set_closure_cell(cell, value): - # Create a function using the set_first_freevar_code, - # whose first closure cell is `cell`. Calling it will - # change the value of that cell. - setter = types.FunctionType( - set_first_freevar_code, {}, "setter", (), (cell,) - ) - # And call it to set the cell. - setter(value) - - # Make sure it works on this interpreter: - def make_func_with_cell(): - x = None - - def func(): - return x # pragma: no cover - - return func - - if PY2: - cell = make_func_with_cell().func_closure[0] - else: - cell = make_func_with_cell().__closure__[0] - set_closure_cell(cell, 100) - if cell.cell_contents != 100: - raise AssertionError # pragma: no cover - - except Exception: - return just_warn - else: - return set_closure_cell - - -set_closure_cell = make_set_closure_cell() - -# Thread-local global to track attrs instances which are already being repr'd. -# This is needed because there is no other (thread-safe) way to pass info -# about the instances that are already being repr'd through the call stack -# in order to ensure we don't perform infinite recursion. -# -# For instance, if an instance contains a dict which contains that instance, -# we need to know that we're already repr'ing the outside instance from within -# the dict's repr() call. -# -# This lives here rather than in _make.py so that the functions in _make.py -# don't have a direct reference to the thread-local in their globals dict. -# If they have such a reference, it breaks cloudpickle. -repr_context = threading.local() diff --git a/client/ayon_core/vendor/python/python_2/attr/_config.py b/client/ayon_core/vendor/python/python_2/attr/_config.py deleted file mode 100644 index fc9be29d00..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_config.py +++ /dev/null @@ -1,33 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - - -__all__ = ["set_run_validators", "get_run_validators"] - -_run_validators = True - - -def set_run_validators(run): - """ - Set whether or not validators are run. By default, they are run. - - .. deprecated:: 21.3.0 It will not be removed, but it also will not be - moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` - instead. - """ - if not isinstance(run, bool): - raise TypeError("'run' must be bool.") - global _run_validators - _run_validators = run - - -def get_run_validators(): - """ - Return whether or not validators are run. - - .. deprecated:: 21.3.0 It will not be removed, but it also will not be - moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` - instead. - """ - return _run_validators diff --git a/client/ayon_core/vendor/python/python_2/attr/_funcs.py b/client/ayon_core/vendor/python/python_2/attr/_funcs.py deleted file mode 100644 index 4c90085a40..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_funcs.py +++ /dev/null @@ -1,422 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import copy - -from ._compat import iteritems -from ._make import NOTHING, _obj_setattr, fields -from .exceptions import AttrsAttributeNotFoundError - - -def asdict( - inst, - recurse=True, - filter=None, - dict_factory=dict, - retain_collection_types=False, - value_serializer=None, -): - """ - Return the ``attrs`` attribute values of *inst* as a dict. - - Optionally recurse into other ``attrs``-decorated classes. - - :param inst: Instance of an ``attrs``-decorated class. - :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. - :param callable filter: A callable whose return code determines whether an - attribute or element is included (``True``) or dropped (``False``). Is - called with the `attrs.Attribute` as the first argument and the - value as the second argument. - :param callable dict_factory: A callable to produce dictionaries from. For - example, to produce ordered dictionaries instead of normal Python - dictionaries, pass in ``collections.OrderedDict``. - :param bool retain_collection_types: Do not convert to ``list`` when - encountering an attribute whose type is ``tuple`` or ``set``. Only - meaningful if ``recurse`` is ``True``. - :param Optional[callable] value_serializer: A hook that is called for every - attribute or dict key/value. It receives the current instance, field - and value and must return the (updated) value. The hook is run *after* - the optional *filter* has been applied. - - :rtype: return type of *dict_factory* - - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - .. versionadded:: 16.0.0 *dict_factory* - .. versionadded:: 16.1.0 *retain_collection_types* - .. versionadded:: 20.3.0 *value_serializer* - .. versionadded:: 21.3.0 If a dict has a collection for a key, it is - serialized as a tuple. - """ - attrs = fields(inst.__class__) - rv = dict_factory() - for a in attrs: - v = getattr(inst, a.name) - if filter is not None and not filter(a, v): - continue - - if value_serializer is not None: - v = value_serializer(inst, a, v) - - if recurse is True: - if has(v.__class__): - rv[a.name] = asdict( - v, - recurse=True, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - elif isinstance(v, (tuple, list, set, frozenset)): - cf = v.__class__ if retain_collection_types is True else list - rv[a.name] = cf( - [ - _asdict_anything( - i, - is_key=False, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - for i in v - ] - ) - elif isinstance(v, dict): - df = dict_factory - rv[a.name] = df( - ( - _asdict_anything( - kk, - is_key=True, - filter=filter, - dict_factory=df, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ), - _asdict_anything( - vv, - is_key=False, - filter=filter, - dict_factory=df, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ), - ) - for kk, vv in iteritems(v) - ) - else: - rv[a.name] = v - else: - rv[a.name] = v - return rv - - -def _asdict_anything( - val, - is_key, - filter, - dict_factory, - retain_collection_types, - value_serializer, -): - """ - ``asdict`` only works on attrs instances, this works on anything. - """ - if getattr(val.__class__, "__attrs_attrs__", None) is not None: - # Attrs class. - rv = asdict( - val, - recurse=True, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - elif isinstance(val, (tuple, list, set, frozenset)): - if retain_collection_types is True: - cf = val.__class__ - elif is_key: - cf = tuple - else: - cf = list - - rv = cf( - [ - _asdict_anything( - i, - is_key=False, - filter=filter, - dict_factory=dict_factory, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ) - for i in val - ] - ) - elif isinstance(val, dict): - df = dict_factory - rv = df( - ( - _asdict_anything( - kk, - is_key=True, - filter=filter, - dict_factory=df, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ), - _asdict_anything( - vv, - is_key=False, - filter=filter, - dict_factory=df, - retain_collection_types=retain_collection_types, - value_serializer=value_serializer, - ), - ) - for kk, vv in iteritems(val) - ) - else: - rv = val - if value_serializer is not None: - rv = value_serializer(None, None, rv) - - return rv - - -def astuple( - inst, - recurse=True, - filter=None, - tuple_factory=tuple, - retain_collection_types=False, -): - """ - Return the ``attrs`` attribute values of *inst* as a tuple. - - Optionally recurse into other ``attrs``-decorated classes. - - :param inst: Instance of an ``attrs``-decorated class. - :param bool recurse: Recurse into classes that are also - ``attrs``-decorated. - :param callable filter: A callable whose return code determines whether an - attribute or element is included (``True``) or dropped (``False``). Is - called with the `attrs.Attribute` as the first argument and the - value as the second argument. - :param callable tuple_factory: A callable to produce tuples from. For - example, to produce lists instead of tuples. - :param bool retain_collection_types: Do not convert to ``list`` - or ``dict`` when encountering an attribute which type is - ``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is - ``True``. - - :rtype: return type of *tuple_factory* - - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - .. versionadded:: 16.2.0 - """ - attrs = fields(inst.__class__) - rv = [] - retain = retain_collection_types # Very long. :/ - for a in attrs: - v = getattr(inst, a.name) - if filter is not None and not filter(a, v): - continue - if recurse is True: - if has(v.__class__): - rv.append( - astuple( - v, - recurse=True, - filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - ) - elif isinstance(v, (tuple, list, set, frozenset)): - cf = v.__class__ if retain is True else list - rv.append( - cf( - [ - astuple( - j, - recurse=True, - filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(j.__class__) - else j - for j in v - ] - ) - ) - elif isinstance(v, dict): - df = v.__class__ if retain is True else dict - rv.append( - df( - ( - astuple( - kk, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(kk.__class__) - else kk, - astuple( - vv, - tuple_factory=tuple_factory, - retain_collection_types=retain, - ) - if has(vv.__class__) - else vv, - ) - for kk, vv in iteritems(v) - ) - ) - else: - rv.append(v) - else: - rv.append(v) - - return rv if tuple_factory is list else tuple_factory(rv) - - -def has(cls): - """ - Check whether *cls* is a class with ``attrs`` attributes. - - :param type cls: Class to introspect. - :raise TypeError: If *cls* is not a class. - - :rtype: bool - """ - return getattr(cls, "__attrs_attrs__", None) is not None - - -def assoc(inst, **changes): - """ - Copy *inst* and apply *changes*. - - :param inst: Instance of a class with ``attrs`` attributes. - :param changes: Keyword changes in the new copy. - - :return: A copy of inst with *changes* incorporated. - - :raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't - be found on *cls*. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - .. deprecated:: 17.1.0 - Use `attrs.evolve` instead if you can. - This function will not be removed du to the slightly different approach - compared to `attrs.evolve`. - """ - import warnings - - warnings.warn( - "assoc is deprecated and will be removed after 2018/01.", - DeprecationWarning, - stacklevel=2, - ) - new = copy.copy(inst) - attrs = fields(inst.__class__) - for k, v in iteritems(changes): - a = getattr(attrs, k, NOTHING) - if a is NOTHING: - raise AttrsAttributeNotFoundError( - "{k} is not an attrs attribute on {cl}.".format( - k=k, cl=new.__class__ - ) - ) - _obj_setattr(new, k, v) - return new - - -def evolve(inst, **changes): - """ - Create a new instance, based on *inst* with *changes* applied. - - :param inst: Instance of a class with ``attrs`` attributes. - :param changes: Keyword changes in the new copy. - - :return: A copy of inst with *changes* incorporated. - - :raise TypeError: If *attr_name* couldn't be found in the class - ``__init__``. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - .. versionadded:: 17.1.0 - """ - cls = inst.__class__ - attrs = fields(cls) - for a in attrs: - if not a.init: - continue - attr_name = a.name # To deal with private attributes. - init_name = attr_name if attr_name[0] != "_" else attr_name[1:] - if init_name not in changes: - changes[init_name] = getattr(inst, attr_name) - - return cls(**changes) - - -def resolve_types(cls, globalns=None, localns=None, attribs=None): - """ - Resolve any strings and forward annotations in type annotations. - - This is only required if you need concrete types in `Attribute`'s *type* - field. In other words, you don't need to resolve your types if you only - use them for static type checking. - - With no arguments, names will be looked up in the module in which the class - was created. If this is not what you want, e.g. if the name only exists - inside a method, you may pass *globalns* or *localns* to specify other - dictionaries in which to look up these names. See the docs of - `typing.get_type_hints` for more details. - - :param type cls: Class to resolve. - :param Optional[dict] globalns: Dictionary containing global variables. - :param Optional[dict] localns: Dictionary containing local variables. - :param Optional[list] attribs: List of attribs for the given class. - This is necessary when calling from inside a ``field_transformer`` - since *cls* is not an ``attrs`` class yet. - - :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class and you didn't pass any attribs. - :raise NameError: If types cannot be resolved because of missing variables. - - :returns: *cls* so you can use this function also as a class decorator. - Please note that you have to apply it **after** `attrs.define`. That - means the decorator has to come in the line **before** `attrs.define`. - - .. versionadded:: 20.1.0 - .. versionadded:: 21.1.0 *attribs* - - """ - # Since calling get_type_hints is expensive we cache whether we've - # done it already. - if getattr(cls, "__attrs_types_resolved__", None) != cls: - import typing - - hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) - for field in fields(cls) if attribs is None else attribs: - if field.name in hints: - # Since fields have been frozen we must work around it. - _obj_setattr(field, "type", hints[field.name]) - # We store the class we resolved so that subclasses know they haven't - # been resolved. - cls.__attrs_types_resolved__ = cls - - # Return the class so you can use it as a decorator too. - return cls diff --git a/client/ayon_core/vendor/python/python_2/attr/_make.py b/client/ayon_core/vendor/python/python_2/attr/_make.py deleted file mode 100644 index d46f8a3e7a..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_make.py +++ /dev/null @@ -1,3173 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -import copy -import inspect -import linecache -import sys -import warnings - -from operator import itemgetter - -# We need to import _compat itself in addition to the _compat members to avoid -# having the thread-local in the globals here. -from . import _compat, _config, setters -from ._compat import ( - HAS_F_STRINGS, - PY2, - PY310, - PYPY, - isclass, - iteritems, - metadata_proxy, - new_class, - ordered_dict, - set_closure_cell, -) -from .exceptions import ( - DefaultAlreadySetError, - FrozenInstanceError, - NotAnAttrsClassError, - PythonTooOldError, - UnannotatedAttributeError, -) - - -if not PY2: - import typing - - -# This is used at least twice, so cache it here. -_obj_setattr = object.__setattr__ -_init_converter_pat = "__attr_converter_%s" -_init_factory_pat = "__attr_factory_{}" -_tuple_property_pat = ( - " {attr_name} = _attrs_property(_attrs_itemgetter({index}))" -) -_classvar_prefixes = ( - "typing.ClassVar", - "t.ClassVar", - "ClassVar", - "typing_extensions.ClassVar", -) -# we don't use a double-underscore prefix because that triggers -# name mangling when trying to create a slot for the field -# (when slots=True) -_hash_cache_field = "_attrs_cached_hash" - -_empty_metadata_singleton = metadata_proxy({}) - -# Unique object for unequivocal getattr() defaults. -_sentinel = object() - -_ng_default_on_setattr = setters.pipe(setters.convert, setters.validate) - - -class _Nothing(object): - """ - Sentinel class to indicate the lack of a value when ``None`` is ambiguous. - - ``_Nothing`` is a singleton. There is only ever one of it. - - .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. - """ - - _singleton = None - - def __new__(cls): - if _Nothing._singleton is None: - _Nothing._singleton = super(_Nothing, cls).__new__(cls) - return _Nothing._singleton - - def __repr__(self): - return "NOTHING" - - def __bool__(self): - return False - - def __len__(self): - return 0 # __bool__ for Python 2 - - -NOTHING = _Nothing() -""" -Sentinel to indicate the lack of a value when ``None`` is ambiguous. -""" - - -class _CacheHashWrapper(int): - """ - An integer subclass that pickles / copies as None - - This is used for non-slots classes with ``cache_hash=True``, to avoid - serializing a potentially (even likely) invalid hash value. Since ``None`` - is the default value for uncalculated hashes, whenever this is copied, - the copy's value for the hash should automatically reset. - - See GH #613 for more details. - """ - - if PY2: - # For some reason `type(None)` isn't callable in Python 2, but we don't - # actually need a constructor for None objects, we just need any - # available function that returns None. - def __reduce__(self, _none_constructor=getattr, _args=(0, "", None)): - return _none_constructor, _args - - else: - - def __reduce__(self, _none_constructor=type(None), _args=()): - return _none_constructor, _args - - -def attrib( - default=NOTHING, - validator=None, - repr=True, - cmp=None, - hash=None, - init=True, - metadata=None, - type=None, - converter=None, - factory=None, - kw_only=False, - eq=None, - order=None, - on_setattr=None, -): - """ - Create a new attribute on a class. - - .. warning:: - - Does *not* do anything unless the class is also decorated with - `attr.s`! - - :param default: A value that is used if an ``attrs``-generated ``__init__`` - is used and no value is passed while instantiating or the attribute is - excluded using ``init=False``. - - If the value is an instance of `attrs.Factory`, its callable will be - used to construct a new value (useful for mutable data types like lists - or dicts). - - If a default is not set (or set manually to `attrs.NOTHING`), a value - *must* be supplied when instantiating; otherwise a `TypeError` - will be raised. - - The default can also be set using decorator notation as shown below. - - :type default: Any value - - :param callable factory: Syntactic sugar for - ``default=attr.Factory(factory)``. - - :param validator: `callable` that is called by ``attrs``-generated - ``__init__`` methods after the instance has been initialized. They - receive the initialized instance, the :func:`~attrs.Attribute`, and the - passed value. - - The return value is *not* inspected so the validator has to throw an - exception itself. - - If a `list` is passed, its items are treated as validators and must - all pass. - - Validators can be globally disabled and re-enabled using - `get_run_validators`. - - The validator can also be set using decorator notation as shown below. - - :type validator: `callable` or a `list` of `callable`\\ s. - - :param repr: Include this attribute in the generated ``__repr__`` - method. If ``True``, include the attribute; if ``False``, omit it. By - default, the built-in ``repr()`` function is used. To override how the - attribute value is formatted, pass a ``callable`` that takes a single - value and returns a string. Note that the resulting string is used - as-is, i.e. it will be used directly *instead* of calling ``repr()`` - (the default). - :type repr: a `bool` or a `callable` to use a custom function. - - :param eq: If ``True`` (default), include this attribute in the - generated ``__eq__`` and ``__ne__`` methods that check two instances - for equality. To override how the attribute value is compared, - pass a ``callable`` that takes a single value and returns the value - to be compared. - :type eq: a `bool` or a `callable`. - - :param order: If ``True`` (default), include this attributes in the - generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. - To override how the attribute value is ordered, - pass a ``callable`` that takes a single value and returns the value - to be ordered. - :type order: a `bool` or a `callable`. - - :param cmp: Setting *cmp* is equivalent to setting *eq* and *order* to the - same value. Must not be mixed with *eq* or *order*. - :type cmp: a `bool` or a `callable`. - - :param Optional[bool] hash: Include this attribute in the generated - ``__hash__`` method. If ``None`` (default), mirror *eq*'s value. This - is the correct behavior according the Python spec. Setting this value - to anything else than ``None`` is *discouraged*. - :param bool init: Include this attribute in the generated ``__init__`` - method. It is possible to set this to ``False`` and set a default - value. In that case this attributed is unconditionally initialized - with the specified default value or factory. - :param callable converter: `callable` that is called by - ``attrs``-generated ``__init__`` methods to convert attribute's value - to the desired format. It is given the passed-in value, and the - returned value will be used as the new value of the attribute. The - value is converted before being passed to the validator, if any. - :param metadata: An arbitrary mapping, to be used by third-party - components. See `extending_metadata`. - :param type: The type of the attribute. In Python 3.6 or greater, the - preferred method to specify the type is using a variable annotation - (see `PEP 526 `_). - This argument is provided for backward compatibility. - Regardless of the approach used, the type will be stored on - ``Attribute.type``. - - Please note that ``attrs`` doesn't do anything with this metadata by - itself. You can use it as part of your own code or for - `static type checking `. - :param kw_only: Make this attribute keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). - :param on_setattr: Allows to overwrite the *on_setattr* setting from - `attr.s`. If left `None`, the *on_setattr* value from `attr.s` is used. - Set to `attrs.setters.NO_OP` to run **no** `setattr` hooks for this - attribute -- regardless of the setting in `attr.s`. - :type on_setattr: `callable`, or a list of callables, or `None`, or - `attrs.setters.NO_OP` - - .. versionadded:: 15.2.0 *convert* - .. versionadded:: 16.3.0 *metadata* - .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. - .. versionchanged:: 17.1.0 - *hash* is ``None`` and therefore mirrors *eq* by default. - .. versionadded:: 17.3.0 *type* - .. deprecated:: 17.4.0 *convert* - .. versionadded:: 17.4.0 *converter* as a replacement for the deprecated - *convert* to achieve consistency with other noun-based arguments. - .. versionadded:: 18.1.0 - ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. - .. versionadded:: 18.2.0 *kw_only* - .. versionchanged:: 19.2.0 *convert* keyword argument removed. - .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. - .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. - .. versionadded:: 19.2.0 *eq* and *order* - .. versionadded:: 20.1.0 *on_setattr* - .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 - .. versionchanged:: 21.1.0 - *eq*, *order*, and *cmp* also accept a custom callable - .. versionchanged:: 21.1.0 *cmp* undeprecated - """ - eq, eq_key, order, order_key = _determine_attrib_eq_order( - cmp, eq, order, True - ) - - if hash is not None and hash is not True and hash is not False: - raise TypeError( - "Invalid value for hash. Must be True, False, or None." - ) - - if factory is not None: - if default is not NOTHING: - raise ValueError( - "The `default` and `factory` arguments are mutually " - "exclusive." - ) - if not callable(factory): - raise ValueError("The `factory` argument must be a callable.") - default = Factory(factory) - - if metadata is None: - metadata = {} - - # Apply syntactic sugar by auto-wrapping. - if isinstance(on_setattr, (list, tuple)): - on_setattr = setters.pipe(*on_setattr) - - if validator and isinstance(validator, (list, tuple)): - validator = and_(*validator) - - if converter and isinstance(converter, (list, tuple)): - converter = pipe(*converter) - - return _CountingAttr( - default=default, - validator=validator, - repr=repr, - cmp=None, - hash=hash, - init=init, - converter=converter, - metadata=metadata, - type=type, - kw_only=kw_only, - eq=eq, - eq_key=eq_key, - order=order, - order_key=order_key, - on_setattr=on_setattr, - ) - - -def _compile_and_eval(script, globs, locs=None, filename=""): - """ - "Exec" the script with the given global (globs) and local (locs) variables. - """ - bytecode = compile(script, filename, "exec") - eval(bytecode, globs, locs) - - -def _make_method(name, script, filename, globs=None): - """ - Create the method with the script given and return the method object. - """ - locs = {} - if globs is None: - globs = {} - - # In order of debuggers like PDB being able to step through the code, - # we add a fake linecache entry. - count = 1 - base_filename = filename - while True: - linecache_tuple = ( - len(script), - None, - script.splitlines(True), - filename, - ) - old_val = linecache.cache.setdefault(filename, linecache_tuple) - if old_val == linecache_tuple: - break - else: - filename = "{}-{}>".format(base_filename[:-1], count) - count += 1 - - _compile_and_eval(script, globs, locs, filename) - - return locs[name] - - -def _make_attr_tuple_class(cls_name, attr_names): - """ - Create a tuple subclass to hold `Attribute`s for an `attrs` class. - - The subclass is a bare tuple with properties for names. - - class MyClassAttributes(tuple): - __slots__ = () - x = property(itemgetter(0)) - """ - attr_class_name = "{}Attributes".format(cls_name) - attr_class_template = [ - "class {}(tuple):".format(attr_class_name), - " __slots__ = ()", - ] - if attr_names: - for i, attr_name in enumerate(attr_names): - attr_class_template.append( - _tuple_property_pat.format(index=i, attr_name=attr_name) - ) - else: - attr_class_template.append(" pass") - globs = {"_attrs_itemgetter": itemgetter, "_attrs_property": property} - _compile_and_eval("\n".join(attr_class_template), globs) - return globs[attr_class_name] - - -# Tuple class for extracted attributes from a class definition. -# `base_attrs` is a subset of `attrs`. -_Attributes = _make_attr_tuple_class( - "_Attributes", - [ - # all attributes to build dunder methods for - "attrs", - # attributes that have been inherited - "base_attrs", - # map inherited attributes to their originating classes - "base_attrs_map", - ], -) - - -def _is_class_var(annot): - """ - Check whether *annot* is a typing.ClassVar. - - The string comparison hack is used to avoid evaluating all string - annotations which would put attrs-based classes at a performance - disadvantage compared to plain old classes. - """ - annot = str(annot) - - # Annotation can be quoted. - if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): - annot = annot[1:-1] - - return annot.startswith(_classvar_prefixes) - - -def _has_own_attribute(cls, attrib_name): - """ - Check whether *cls* defines *attrib_name* (and doesn't just inherit it). - - Requires Python 3. - """ - attr = getattr(cls, attrib_name, _sentinel) - if attr is _sentinel: - return False - - for base_cls in cls.__mro__[1:]: - a = getattr(base_cls, attrib_name, None) - if attr is a: - return False - - return True - - -def _get_annotations(cls): - """ - Get annotations for *cls*. - """ - if _has_own_attribute(cls, "__annotations__"): - return cls.__annotations__ - - return {} - - -def _counter_getter(e): - """ - Key function for sorting to avoid re-creating a lambda for every class. - """ - return e[1].counter - - -def _collect_base_attrs(cls, taken_attr_names): - """ - Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. - """ - base_attrs = [] - base_attr_map = {} # A dictionary of base attrs to their classes. - - # Traverse the MRO and collect attributes. - for base_cls in reversed(cls.__mro__[1:-1]): - for a in getattr(base_cls, "__attrs_attrs__", []): - if a.inherited or a.name in taken_attr_names: - continue - - a = a.evolve(inherited=True) - base_attrs.append(a) - base_attr_map[a.name] = base_cls - - # For each name, only keep the freshest definition i.e. the furthest at the - # back. base_attr_map is fine because it gets overwritten with every new - # instance. - filtered = [] - seen = set() - for a in reversed(base_attrs): - if a.name in seen: - continue - filtered.insert(0, a) - seen.add(a.name) - - return filtered, base_attr_map - - -def _collect_base_attrs_broken(cls, taken_attr_names): - """ - Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. - - N.B. *taken_attr_names* will be mutated. - - Adhere to the old incorrect behavior. - - Notably it collects from the front and considers inherited attributes which - leads to the buggy behavior reported in #428. - """ - base_attrs = [] - base_attr_map = {} # A dictionary of base attrs to their classes. - - # Traverse the MRO and collect attributes. - for base_cls in cls.__mro__[1:-1]: - for a in getattr(base_cls, "__attrs_attrs__", []): - if a.name in taken_attr_names: - continue - - a = a.evolve(inherited=True) - taken_attr_names.add(a.name) - base_attrs.append(a) - base_attr_map[a.name] = base_cls - - return base_attrs, base_attr_map - - -def _transform_attrs( - cls, these, auto_attribs, kw_only, collect_by_mro, field_transformer -): - """ - Transform all `_CountingAttr`s on a class into `Attribute`s. - - If *these* is passed, use that and don't look for them on the class. - - *collect_by_mro* is True, collect them in the correct MRO order, otherwise - use the old -- incorrect -- order. See #428. - - Return an `_Attributes`. - """ - cd = cls.__dict__ - anns = _get_annotations(cls) - - if these is not None: - ca_list = [(name, ca) for name, ca in iteritems(these)] - - if not isinstance(these, ordered_dict): - ca_list.sort(key=_counter_getter) - elif auto_attribs is True: - ca_names = { - name - for name, attr in cd.items() - if isinstance(attr, _CountingAttr) - } - ca_list = [] - annot_names = set() - for attr_name, type in anns.items(): - if _is_class_var(type): - continue - annot_names.add(attr_name) - a = cd.get(attr_name, NOTHING) - - if not isinstance(a, _CountingAttr): - if a is NOTHING: - a = attrib() - else: - a = attrib(default=a) - ca_list.append((attr_name, a)) - - unannotated = ca_names - annot_names - if len(unannotated) > 0: - raise UnannotatedAttributeError( - "The following `attr.ib`s lack a type annotation: " - + ", ".join( - sorted(unannotated, key=lambda n: cd.get(n).counter) - ) - + "." - ) - else: - ca_list = sorted( - ( - (name, attr) - for name, attr in cd.items() - if isinstance(attr, _CountingAttr) - ), - key=lambda e: e[1].counter, - ) - - own_attrs = [ - Attribute.from_counting_attr( - name=attr_name, ca=ca, type=anns.get(attr_name) - ) - for attr_name, ca in ca_list - ] - - if collect_by_mro: - base_attrs, base_attr_map = _collect_base_attrs( - cls, {a.name for a in own_attrs} - ) - else: - base_attrs, base_attr_map = _collect_base_attrs_broken( - cls, {a.name for a in own_attrs} - ) - - if kw_only: - own_attrs = [a.evolve(kw_only=True) for a in own_attrs] - base_attrs = [a.evolve(kw_only=True) for a in base_attrs] - - attrs = base_attrs + own_attrs - - # Mandatory vs non-mandatory attr order only matters when they are part of - # the __init__ signature and when they aren't kw_only (which are moved to - # the end and can be mandatory or non-mandatory in any order, as they will - # be specified as keyword args anyway). Check the order of those attrs: - had_default = False - for a in (a for a in attrs if a.init is not False and a.kw_only is False): - if had_default is True and a.default is NOTHING: - raise ValueError( - "No mandatory attributes allowed after an attribute with a " - "default value or factory. Attribute in question: %r" % (a,) - ) - - if had_default is False and a.default is not NOTHING: - had_default = True - - if field_transformer is not None: - attrs = field_transformer(cls, attrs) - - # Create AttrsClass *after* applying the field_transformer since it may - # add or remove attributes! - attr_names = [a.name for a in attrs] - AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) - - return _Attributes((AttrsClass(attrs), base_attrs, base_attr_map)) - - -if PYPY: - - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - if isinstance(self, BaseException) and name in ( - "__cause__", - "__context__", - ): - BaseException.__setattr__(self, name, value) - return - - raise FrozenInstanceError() - -else: - - def _frozen_setattrs(self, name, value): - """ - Attached to frozen classes as __setattr__. - """ - raise FrozenInstanceError() - - -def _frozen_delattrs(self, name): - """ - Attached to frozen classes as __delattr__. - """ - raise FrozenInstanceError() - - -class _ClassBuilder(object): - """ - Iteratively build *one* class. - """ - - __slots__ = ( - "_attr_names", - "_attrs", - "_base_attr_map", - "_base_names", - "_cache_hash", - "_cls", - "_cls_dict", - "_delete_attribs", - "_frozen", - "_has_pre_init", - "_has_post_init", - "_is_exc", - "_on_setattr", - "_slots", - "_weakref_slot", - "_wrote_own_setattr", - "_has_custom_setattr", - ) - - def __init__( - self, - cls, - these, - slots, - frozen, - weakref_slot, - getstate_setstate, - auto_attribs, - kw_only, - cache_hash, - is_exc, - collect_by_mro, - on_setattr, - has_custom_setattr, - field_transformer, - ): - attrs, base_attrs, base_map = _transform_attrs( - cls, - these, - auto_attribs, - kw_only, - collect_by_mro, - field_transformer, - ) - - self._cls = cls - self._cls_dict = dict(cls.__dict__) if slots else {} - self._attrs = attrs - self._base_names = set(a.name for a in base_attrs) - self._base_attr_map = base_map - self._attr_names = tuple(a.name for a in attrs) - self._slots = slots - self._frozen = frozen - self._weakref_slot = weakref_slot - self._cache_hash = cache_hash - self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) - self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) - self._delete_attribs = not bool(these) - self._is_exc = is_exc - self._on_setattr = on_setattr - - self._has_custom_setattr = has_custom_setattr - self._wrote_own_setattr = False - - self._cls_dict["__attrs_attrs__"] = self._attrs - - if frozen: - self._cls_dict["__setattr__"] = _frozen_setattrs - self._cls_dict["__delattr__"] = _frozen_delattrs - - self._wrote_own_setattr = True - elif on_setattr in ( - _ng_default_on_setattr, - setters.validate, - setters.convert, - ): - has_validator = has_converter = False - for a in attrs: - if a.validator is not None: - has_validator = True - if a.converter is not None: - has_converter = True - - if has_validator and has_converter: - break - if ( - ( - on_setattr == _ng_default_on_setattr - and not (has_validator or has_converter) - ) - or (on_setattr == setters.validate and not has_validator) - or (on_setattr == setters.convert and not has_converter) - ): - # If class-level on_setattr is set to convert + validate, but - # there's no field to convert or validate, pretend like there's - # no on_setattr. - self._on_setattr = None - - if getstate_setstate: - ( - self._cls_dict["__getstate__"], - self._cls_dict["__setstate__"], - ) = self._make_getstate_setstate() - - def __repr__(self): - return "<_ClassBuilder(cls={cls})>".format(cls=self._cls.__name__) - - def build_class(self): - """ - Finalize class based on the accumulated configuration. - - Builder cannot be used after calling this method. - """ - if self._slots is True: - return self._create_slots_class() - else: - return self._patch_original_class() - - def _patch_original_class(self): - """ - Apply accumulated methods and return the class. - """ - cls = self._cls - base_names = self._base_names - - # Clean class of attribute definitions (`attr.ib()`s). - if self._delete_attribs: - for name in self._attr_names: - if ( - name not in base_names - and getattr(cls, name, _sentinel) is not _sentinel - ): - try: - delattr(cls, name) - except AttributeError: - # This can happen if a base class defines a class - # variable and we want to set an attribute with the - # same name by using only a type annotation. - pass - - # Attach our dunder methods. - for name, value in self._cls_dict.items(): - setattr(cls, name, value) - - # If we've inherited an attrs __setattr__ and don't write our own, - # reset it to object's. - if not self._wrote_own_setattr and getattr( - cls, "__attrs_own_setattr__", False - ): - cls.__attrs_own_setattr__ = False - - if not self._has_custom_setattr: - cls.__setattr__ = object.__setattr__ - - return cls - - def _create_slots_class(self): - """ - Build and return a new class with a `__slots__` attribute. - """ - cd = { - k: v - for k, v in iteritems(self._cls_dict) - if k not in tuple(self._attr_names) + ("__dict__", "__weakref__") - } - - # If our class doesn't have its own implementation of __setattr__ - # (either from the user or by us), check the bases, if one of them has - # an attrs-made __setattr__, that needs to be reset. We don't walk the - # MRO because we only care about our immediate base classes. - # XXX: This can be confused by subclassing a slotted attrs class with - # XXX: a non-attrs class and subclass the resulting class with an attrs - # XXX: class. See `test_slotted_confused` for details. For now that's - # XXX: OK with us. - if not self._wrote_own_setattr: - cd["__attrs_own_setattr__"] = False - - if not self._has_custom_setattr: - for base_cls in self._cls.__bases__: - if base_cls.__dict__.get("__attrs_own_setattr__", False): - cd["__setattr__"] = object.__setattr__ - break - - # Traverse the MRO to collect existing slots - # and check for an existing __weakref__. - existing_slots = dict() - weakref_inherited = False - for base_cls in self._cls.__mro__[1:-1]: - if base_cls.__dict__.get("__weakref__", None) is not None: - weakref_inherited = True - existing_slots.update( - { - name: getattr(base_cls, name) - for name in getattr(base_cls, "__slots__", []) - } - ) - - base_names = set(self._base_names) - - names = self._attr_names - if ( - self._weakref_slot - and "__weakref__" not in getattr(self._cls, "__slots__", ()) - and "__weakref__" not in names - and not weakref_inherited - ): - names += ("__weakref__",) - - # We only add the names of attributes that aren't inherited. - # Setting __slots__ to inherited attributes wastes memory. - slot_names = [name for name in names if name not in base_names] - # There are slots for attributes from current class - # that are defined in parent classes. - # As their descriptors may be overriden by a child class, - # we collect them here and update the class dict - reused_slots = { - slot: slot_descriptor - for slot, slot_descriptor in iteritems(existing_slots) - if slot in slot_names - } - slot_names = [name for name in slot_names if name not in reused_slots] - cd.update(reused_slots) - if self._cache_hash: - slot_names.append(_hash_cache_field) - cd["__slots__"] = tuple(slot_names) - - qualname = getattr(self._cls, "__qualname__", None) - if qualname is not None: - cd["__qualname__"] = qualname - - # Create new class based on old class and our methods. - cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) - - # The following is a fix for - # . On Python 3, - # if a method mentions `__class__` or uses the no-arg super(), the - # compiler will bake a reference to the class in the method itself - # as `method.__closure__`. Since we replace the class with a - # clone, we rewrite these references so it keeps working. - for item in cls.__dict__.values(): - if isinstance(item, (classmethod, staticmethod)): - # Class- and staticmethods hide their functions inside. - # These might need to be rewritten as well. - closure_cells = getattr(item.__func__, "__closure__", None) - elif isinstance(item, property): - # Workaround for property `super()` shortcut (PY3-only). - # There is no universal way for other descriptors. - closure_cells = getattr(item.fget, "__closure__", None) - else: - closure_cells = getattr(item, "__closure__", None) - - if not closure_cells: # Catch None or the empty list. - continue - for cell in closure_cells: - try: - match = cell.cell_contents is self._cls - except ValueError: # ValueError: Cell is empty - pass - else: - if match: - set_closure_cell(cell, cls) - - return cls - - def add_repr(self, ns): - self._cls_dict["__repr__"] = self._add_method_dunders( - _make_repr(self._attrs, ns, self._cls) - ) - return self - - def add_str(self): - repr = self._cls_dict.get("__repr__") - if repr is None: - raise ValueError( - "__str__ can only be generated if a __repr__ exists." - ) - - def __str__(self): - return self.__repr__() - - self._cls_dict["__str__"] = self._add_method_dunders(__str__) - return self - - def _make_getstate_setstate(self): - """ - Create custom __setstate__ and __getstate__ methods. - """ - # __weakref__ is not writable. - state_attr_names = tuple( - an for an in self._attr_names if an != "__weakref__" - ) - - def slots_getstate(self): - """ - Automatically created by attrs. - """ - return tuple(getattr(self, name) for name in state_attr_names) - - hash_caching_enabled = self._cache_hash - - def slots_setstate(self, state): - """ - Automatically created by attrs. - """ - __bound_setattr = _obj_setattr.__get__(self, Attribute) - for name, value in zip(state_attr_names, state): - __bound_setattr(name, value) - - # The hash code cache is not included when the object is - # serialized, but it still needs to be initialized to None to - # indicate that the first call to __hash__ should be a cache - # miss. - if hash_caching_enabled: - __bound_setattr(_hash_cache_field, None) - - return slots_getstate, slots_setstate - - def make_unhashable(self): - self._cls_dict["__hash__"] = None - return self - - def add_hash(self): - self._cls_dict["__hash__"] = self._add_method_dunders( - _make_hash( - self._cls, - self._attrs, - frozen=self._frozen, - cache_hash=self._cache_hash, - ) - ) - - return self - - def add_init(self): - self._cls_dict["__init__"] = self._add_method_dunders( - _make_init( - self._cls, - self._attrs, - self._has_pre_init, - self._has_post_init, - self._frozen, - self._slots, - self._cache_hash, - self._base_attr_map, - self._is_exc, - self._on_setattr, - attrs_init=False, - ) - ) - - return self - - def add_match_args(self): - self._cls_dict["__match_args__"] = tuple( - field.name - for field in self._attrs - if field.init and not field.kw_only - ) - - def add_attrs_init(self): - self._cls_dict["__attrs_init__"] = self._add_method_dunders( - _make_init( - self._cls, - self._attrs, - self._has_pre_init, - self._has_post_init, - self._frozen, - self._slots, - self._cache_hash, - self._base_attr_map, - self._is_exc, - self._on_setattr, - attrs_init=True, - ) - ) - - return self - - def add_eq(self): - cd = self._cls_dict - - cd["__eq__"] = self._add_method_dunders( - _make_eq(self._cls, self._attrs) - ) - cd["__ne__"] = self._add_method_dunders(_make_ne()) - - return self - - def add_order(self): - cd = self._cls_dict - - cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( - self._add_method_dunders(meth) - for meth in _make_order(self._cls, self._attrs) - ) - - return self - - def add_setattr(self): - if self._frozen: - return self - - sa_attrs = {} - for a in self._attrs: - on_setattr = a.on_setattr or self._on_setattr - if on_setattr and on_setattr is not setters.NO_OP: - sa_attrs[a.name] = a, on_setattr - - if not sa_attrs: - return self - - if self._has_custom_setattr: - # We need to write a __setattr__ but there already is one! - raise ValueError( - "Can't combine custom __setattr__ with on_setattr hooks." - ) - - # docstring comes from _add_method_dunders - def __setattr__(self, name, val): - try: - a, hook = sa_attrs[name] - except KeyError: - nval = val - else: - nval = hook(self, a, val) - - _obj_setattr(self, name, nval) - - self._cls_dict["__attrs_own_setattr__"] = True - self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) - self._wrote_own_setattr = True - - return self - - def _add_method_dunders(self, method): - """ - Add __module__ and __qualname__ to a *method* if possible. - """ - try: - method.__module__ = self._cls.__module__ - except AttributeError: - pass - - try: - method.__qualname__ = ".".join( - (self._cls.__qualname__, method.__name__) - ) - except AttributeError: - pass - - try: - method.__doc__ = "Method generated by attrs for class %s." % ( - self._cls.__qualname__, - ) - except AttributeError: - pass - - return method - - -_CMP_DEPRECATION = ( - "The usage of `cmp` is deprecated and will be removed on or after " - "2021-06-01. Please use `eq` and `order` instead." -) - - -def _determine_attrs_eq_order(cmp, eq, order, default_eq): - """ - Validate the combination of *cmp*, *eq*, and *order*. Derive the effective - values of eq and order. If *eq* is None, set it to *default_eq*. - """ - if cmp is not None and any((eq is not None, order is not None)): - raise ValueError("Don't mix `cmp` with `eq' and `order`.") - - # cmp takes precedence due to bw-compatibility. - if cmp is not None: - return cmp, cmp - - # If left None, equality is set to the specified default and ordering - # mirrors equality. - if eq is None: - eq = default_eq - - if order is None: - order = eq - - if eq is False and order is True: - raise ValueError("`order` can only be True if `eq` is True too.") - - return eq, order - - -def _determine_attrib_eq_order(cmp, eq, order, default_eq): - """ - Validate the combination of *cmp*, *eq*, and *order*. Derive the effective - values of eq and order. If *eq* is None, set it to *default_eq*. - """ - if cmp is not None and any((eq is not None, order is not None)): - raise ValueError("Don't mix `cmp` with `eq' and `order`.") - - def decide_callable_or_boolean(value): - """ - Decide whether a key function is used. - """ - if callable(value): - value, key = True, value - else: - key = None - return value, key - - # cmp takes precedence due to bw-compatibility. - if cmp is not None: - cmp, cmp_key = decide_callable_or_boolean(cmp) - return cmp, cmp_key, cmp, cmp_key - - # If left None, equality is set to the specified default and ordering - # mirrors equality. - if eq is None: - eq, eq_key = default_eq, None - else: - eq, eq_key = decide_callable_or_boolean(eq) - - if order is None: - order, order_key = eq, eq_key - else: - order, order_key = decide_callable_or_boolean(order) - - if eq is False and order is True: - raise ValueError("`order` can only be True if `eq` is True too.") - - return eq, eq_key, order, order_key - - -def _determine_whether_to_implement( - cls, flag, auto_detect, dunders, default=True -): - """ - Check whether we should implement a set of methods for *cls*. - - *flag* is the argument passed into @attr.s like 'init', *auto_detect* the - same as passed into @attr.s and *dunders* is a tuple of attribute names - whose presence signal that the user has implemented it themselves. - - Return *default* if no reason for either for or against is found. - - auto_detect must be False on Python 2. - """ - if flag is True or flag is False: - return flag - - if flag is None and auto_detect is False: - return default - - # Logically, flag is None and auto_detect is True here. - for dunder in dunders: - if _has_own_attribute(cls, dunder): - return False - - return default - - -def attrs( - maybe_cls=None, - these=None, - repr_ns=None, - repr=None, - cmp=None, - hash=None, - init=None, - slots=False, - frozen=False, - weakref_slot=True, - str=False, - auto_attribs=False, - kw_only=False, - cache_hash=False, - auto_exc=False, - eq=None, - order=None, - auto_detect=False, - collect_by_mro=False, - getstate_setstate=None, - on_setattr=None, - field_transformer=None, - match_args=True, -): - r""" - A class decorator that adds `dunder - `_\ -methods according to the - specified attributes using `attr.ib` or the *these* argument. - - :param these: A dictionary of name to `attr.ib` mappings. This is - useful to avoid the definition of your attributes within the class body - because you can't (e.g. if you want to add ``__repr__`` methods to - Django models) or don't want to. - - If *these* is not ``None``, ``attrs`` will *not* search the class body - for attributes and will *not* remove any attributes from it. - - If *these* is an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the attributes inside *these*. Otherwise the order - of the definition of the attributes is used. - - :type these: `dict` of `str` to `attr.ib` - - :param str repr_ns: When using nested classes, there's no way in Python 2 - to automatically detect that. Therefore it's possible to set the - namespace explicitly for a more meaningful ``repr`` output. - :param bool auto_detect: Instead of setting the *init*, *repr*, *eq*, - *order*, and *hash* arguments explicitly, assume they are set to - ``True`` **unless any** of the involved methods for one of the - arguments is implemented in the *current* class (i.e. it is *not* - inherited from some base class). - - So for example by implementing ``__eq__`` on a class yourself, - ``attrs`` will deduce ``eq=False`` and will create *neither* - ``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible - ``__ne__`` by default, so it *should* be enough to only implement - ``__eq__`` in most cases). - - .. warning:: - - If you prevent ``attrs`` from creating the ordering methods for you - (``order=False``, e.g. by implementing ``__le__``), it becomes - *your* responsibility to make sure its ordering is sound. The best - way is to use the `functools.total_ordering` decorator. - - - Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*, - *cmp*, or *hash* overrides whatever *auto_detect* would determine. - - *auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises - an `attrs.exceptions.PythonTooOldError`. - - :param bool repr: Create a ``__repr__`` method with a human readable - representation of ``attrs`` attributes.. - :param bool str: Create a ``__str__`` method that is identical to - ``__repr__``. This is usually not necessary except for - `Exception`\ s. - :param Optional[bool] eq: If ``True`` or ``None`` (default), add ``__eq__`` - and ``__ne__`` methods that check two instances for equality. - - They compare the instances as if they were tuples of their ``attrs`` - attributes if and only if the types of both classes are *identical*! - :param Optional[bool] order: If ``True``, add ``__lt__``, ``__le__``, - ``__gt__``, and ``__ge__`` methods that behave like *eq* above and - allow instances to be ordered. If ``None`` (default) mirror value of - *eq*. - :param Optional[bool] cmp: Setting *cmp* is equivalent to setting *eq* - and *order* to the same value. Must not be mixed with *eq* or *order*. - :param Optional[bool] hash: If ``None`` (default), the ``__hash__`` method - is generated according how *eq* and *frozen* are set. - - 1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you. - 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to - None, marking it unhashable (which it is). - 3. If *eq* is False, ``__hash__`` will be left untouched meaning the - ``__hash__`` method of the base class will be used (if base class is - ``object``, this means it will fall back to id-based hashing.). - - Although not recommended, you can decide for yourself and force - ``attrs`` to create one (e.g. if the class is immutable even though you - didn't freeze it programmatically) by passing ``True`` or not. Both of - these cases are rather special and should be used carefully. - - See our documentation on `hashing`, Python's documentation on - `object.__hash__`, and the `GitHub issue that led to the default \ - behavior `_ for more - details. - :param bool init: Create a ``__init__`` method that initializes the - ``attrs`` attributes. Leading underscores are stripped for the argument - name. If a ``__attrs_pre_init__`` method exists on the class, it will - be called before the class is initialized. If a ``__attrs_post_init__`` - method exists on the class, it will be called after the class is fully - initialized. - - If ``init`` is ``False``, an ``__attrs_init__`` method will be - injected instead. This allows you to define a custom ``__init__`` - method that can do pre-init work such as ``super().__init__()``, - and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. - :param bool slots: Create a `slotted class ` that's more - memory-efficient. Slotted classes are generally superior to the default - dict classes, but have some gotchas you should know about, so we - encourage you to read the `glossary entry `. - :param bool frozen: Make instances immutable after initialization. If - someone attempts to modify a frozen instance, - `attr.exceptions.FrozenInstanceError` is raised. - - .. note:: - - 1. This is achieved by installing a custom ``__setattr__`` method - on your class, so you can't implement your own. - - 2. True immutability is impossible in Python. - - 3. This *does* have a minor a runtime performance `impact - ` when initializing new instances. In other words: - ``__init__`` is slightly slower with ``frozen=True``. - - 4. If a class is frozen, you cannot modify ``self`` in - ``__attrs_post_init__`` or a self-written ``__init__``. You can - circumvent that limitation by using - ``object.__setattr__(self, "attribute_name", value)``. - - 5. Subclasses of a frozen class are frozen too. - - :param bool weakref_slot: Make instances weak-referenceable. This has no - effect unless ``slots`` is also enabled. - :param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated - attributes (Python 3.6 and later only) from the class body. - - In this case, you **must** annotate every field. If ``attrs`` - encounters a field that is set to an `attr.ib` but lacks a type - annotation, an `attr.exceptions.UnannotatedAttributeError` is - raised. Use ``field_name: typing.Any = attr.ib(...)`` if you don't - want to set a type. - - If you assign a value to those attributes (e.g. ``x: int = 42``), that - value becomes the default value like if it were passed using - ``attr.ib(default=42)``. Passing an instance of `attrs.Factory` also - works as expected in most cases (see warning below). - - Attributes annotated as `typing.ClassVar`, and attributes that are - neither annotated nor set to an `attr.ib` are **ignored**. - - .. warning:: - For features that use the attribute name to create decorators (e.g. - `validators `), you still *must* assign `attr.ib` to - them. Otherwise Python will either not find the name or try to use - the default value to call e.g. ``validator`` on it. - - These errors can be quite confusing and probably the most common bug - report on our bug tracker. - - .. _`PEP 526`: https://www.python.org/dev/peps/pep-0526/ - :param bool kw_only: Make all attributes keyword-only (Python 3+) - in the generated ``__init__`` (if ``init`` is ``False``, this - parameter is ignored). - :param bool cache_hash: Ensure that the object's hash code is computed - only once and stored on the object. If this is set to ``True``, - hashing must be either explicitly or implicitly enabled for this - class. If the hash code is cached, avoid any reassignments of - fields involved in hash code computation or mutations of the objects - those fields point to after object creation. If such changes occur, - the behavior of the object's hash code is undefined. - :param bool auto_exc: If the class subclasses `BaseException` - (which implicitly includes any subclass of any exception), the - following happens to behave like a well-behaved Python exceptions - class: - - - the values for *eq*, *order*, and *hash* are ignored and the - instances compare and hash by the instance's ids (N.B. ``attrs`` will - *not* remove existing implementations of ``__hash__`` or the equality - methods. It just won't add own ones.), - - all attributes that are either passed into ``__init__`` or have a - default value are additionally available as a tuple in the ``args`` - attribute, - - the value of *str* is ignored leaving ``__str__`` to base classes. - :param bool collect_by_mro: Setting this to `True` fixes the way ``attrs`` - collects attributes from base classes. The default behavior is - incorrect in certain cases of multiple inheritance. It should be on by - default but is kept off for backward-compatibility. - - See issue `#428 `_ for - more details. - - :param Optional[bool] getstate_setstate: - .. note:: - This is usually only interesting for slotted classes and you should - probably just set *auto_detect* to `True`. - - If `True`, ``__getstate__`` and - ``__setstate__`` are generated and attached to the class. This is - necessary for slotted classes to be pickleable. If left `None`, it's - `True` by default for slotted classes and ``False`` for dict classes. - - If *auto_detect* is `True`, and *getstate_setstate* is left `None`, - and **either** ``__getstate__`` or ``__setstate__`` is detected directly - on the class (i.e. not inherited), it is set to `False` (this is usually - what you want). - - :param on_setattr: A callable that is run whenever the user attempts to set - an attribute (either by assignment like ``i.x = 42`` or by using - `setattr` like ``setattr(i, "x", 42)``). It receives the same arguments - as validators: the instance, the attribute that is being modified, and - the new value. - - If no exception is raised, the attribute is set to the return value of - the callable. - - If a list of callables is passed, they're automatically wrapped in an - `attrs.setters.pipe`. - - :param Optional[callable] field_transformer: - A function that is called with the original class object and all - fields right before ``attrs`` finalizes the class. You can use - this, e.g., to automatically add converters or validators to - fields based on their types. See `transform-fields` for more details. - - :param bool match_args: - If `True` (default), set ``__match_args__`` on the class to support - `PEP 634 `_ (Structural - Pattern Matching). It is a tuple of all positional-only ``__init__`` - parameter names on Python 3.10 and later. Ignored on older Python - versions. - - .. versionadded:: 16.0.0 *slots* - .. versionadded:: 16.1.0 *frozen* - .. versionadded:: 16.3.0 *str* - .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. - .. versionchanged:: 17.1.0 - *hash* supports ``None`` as value which is also the default now. - .. versionadded:: 17.3.0 *auto_attribs* - .. versionchanged:: 18.1.0 - If *these* is passed, no attributes are deleted from the class body. - .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. - .. versionadded:: 18.2.0 *weakref_slot* - .. deprecated:: 18.2.0 - ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a - `DeprecationWarning` if the classes compared are subclasses of - each other. ``__eq`` and ``__ne__`` never tried to compared subclasses - to each other. - .. versionchanged:: 19.2.0 - ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider - subclasses comparable anymore. - .. versionadded:: 18.2.0 *kw_only* - .. versionadded:: 18.2.0 *cache_hash* - .. versionadded:: 19.1.0 *auto_exc* - .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. - .. versionadded:: 19.2.0 *eq* and *order* - .. versionadded:: 20.1.0 *auto_detect* - .. versionadded:: 20.1.0 *collect_by_mro* - .. versionadded:: 20.1.0 *getstate_setstate* - .. versionadded:: 20.1.0 *on_setattr* - .. versionadded:: 20.3.0 *field_transformer* - .. versionchanged:: 21.1.0 - ``init=False`` injects ``__attrs_init__`` - .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` - .. versionchanged:: 21.1.0 *cmp* undeprecated - .. versionadded:: 21.3.0 *match_args* - """ - if auto_detect and PY2: - raise PythonTooOldError( - "auto_detect only works on Python 3 and later." - ) - - eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) - hash_ = hash # work around the lack of nonlocal - - if isinstance(on_setattr, (list, tuple)): - on_setattr = setters.pipe(*on_setattr) - - def wrap(cls): - - if getattr(cls, "__class__", None) is None: - raise TypeError("attrs only works with new-style classes.") - - is_frozen = frozen or _has_frozen_base_class(cls) - is_exc = auto_exc is True and issubclass(cls, BaseException) - has_own_setattr = auto_detect and _has_own_attribute( - cls, "__setattr__" - ) - - if has_own_setattr and is_frozen: - raise ValueError("Can't freeze a class with a custom __setattr__.") - - builder = _ClassBuilder( - cls, - these, - slots, - is_frozen, - weakref_slot, - _determine_whether_to_implement( - cls, - getstate_setstate, - auto_detect, - ("__getstate__", "__setstate__"), - default=slots, - ), - auto_attribs, - kw_only, - cache_hash, - is_exc, - collect_by_mro, - on_setattr, - has_own_setattr, - field_transformer, - ) - if _determine_whether_to_implement( - cls, repr, auto_detect, ("__repr__",) - ): - builder.add_repr(repr_ns) - if str is True: - builder.add_str() - - eq = _determine_whether_to_implement( - cls, eq_, auto_detect, ("__eq__", "__ne__") - ) - if not is_exc and eq is True: - builder.add_eq() - if not is_exc and _determine_whether_to_implement( - cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__") - ): - builder.add_order() - - builder.add_setattr() - - if ( - hash_ is None - and auto_detect is True - and _has_own_attribute(cls, "__hash__") - ): - hash = False - else: - hash = hash_ - if hash is not True and hash is not False and hash is not None: - # Can't use `hash in` because 1 == True for example. - raise TypeError( - "Invalid value for hash. Must be True, False, or None." - ) - elif hash is False or (hash is None and eq is False) or is_exc: - # Don't do anything. Should fall back to __object__'s __hash__ - # which is by id. - if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " hashing must be either explicitly or implicitly " - "enabled." - ) - elif hash is True or ( - hash is None and eq is True and is_frozen is True - ): - # Build a __hash__ if told so, or if it's safe. - builder.add_hash() - else: - # Raise TypeError on attempts to hash. - if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " hashing must be either explicitly or implicitly " - "enabled." - ) - builder.make_unhashable() - - if _determine_whether_to_implement( - cls, init, auto_detect, ("__init__",) - ): - builder.add_init() - else: - builder.add_attrs_init() - if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " init must be True." - ) - - if ( - PY310 - and match_args - and not _has_own_attribute(cls, "__match_args__") - ): - builder.add_match_args() - - return builder.build_class() - - # maybe_cls's type depends on the usage of the decorator. It's a class - # if it's used as `@attrs` but ``None`` if used as `@attrs()`. - if maybe_cls is None: - return wrap - else: - return wrap(maybe_cls) - - -_attrs = attrs -""" -Internal alias so we can use it in functions that take an argument called -*attrs*. -""" - - -if PY2: - - def _has_frozen_base_class(cls): - """ - Check whether *cls* has a frozen ancestor by looking at its - __setattr__. - """ - return ( - getattr(cls.__setattr__, "__module__", None) - == _frozen_setattrs.__module__ - and cls.__setattr__.__name__ == _frozen_setattrs.__name__ - ) - -else: - - def _has_frozen_base_class(cls): - """ - Check whether *cls* has a frozen ancestor by looking at its - __setattr__. - """ - return cls.__setattr__ == _frozen_setattrs - - -def _generate_unique_filename(cls, func_name): - """ - Create a "filename" suitable for a function being generated. - """ - unique_filename = "".format( - func_name, - cls.__module__, - getattr(cls, "__qualname__", cls.__name__), - ) - return unique_filename - - -def _make_hash(cls, attrs, frozen, cache_hash): - attrs = tuple( - a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) - ) - - tab = " " - - unique_filename = _generate_unique_filename(cls, "hash") - type_hash = hash(unique_filename) - - hash_def = "def __hash__(self" - hash_func = "hash((" - closing_braces = "))" - if not cache_hash: - hash_def += "):" - else: - if not PY2: - hash_def += ", *" - - hash_def += ( - ", _cache_wrapper=" - + "__import__('attr._make')._make._CacheHashWrapper):" - ) - hash_func = "_cache_wrapper(" + hash_func - closing_braces += ")" - - method_lines = [hash_def] - - def append_hash_computation_lines(prefix, indent): - """ - Generate the code for actually computing the hash code. - Below this will either be returned directly or used to compute - a value which is then cached, depending on the value of cache_hash - """ - - method_lines.extend( - [ - indent + prefix + hash_func, - indent + " %d," % (type_hash,), - ] - ) - - for a in attrs: - method_lines.append(indent + " self.%s," % a.name) - - method_lines.append(indent + " " + closing_braces) - - if cache_hash: - method_lines.append(tab + "if self.%s is None:" % _hash_cache_field) - if frozen: - append_hash_computation_lines( - "object.__setattr__(self, '%s', " % _hash_cache_field, tab * 2 - ) - method_lines.append(tab * 2 + ")") # close __setattr__ - else: - append_hash_computation_lines( - "self.%s = " % _hash_cache_field, tab * 2 - ) - method_lines.append(tab + "return self.%s" % _hash_cache_field) - else: - append_hash_computation_lines("return ", tab) - - script = "\n".join(method_lines) - return _make_method("__hash__", script, unique_filename) - - -def _add_hash(cls, attrs): - """ - Add a hash method to *cls*. - """ - cls.__hash__ = _make_hash(cls, attrs, frozen=False, cache_hash=False) - return cls - - -def _make_ne(): - """ - Create __ne__ method. - """ - - def __ne__(self, other): - """ - Check equality and either forward a NotImplemented or - return the result negated. - """ - result = self.__eq__(other) - if result is NotImplemented: - return NotImplemented - - return not result - - return __ne__ - - -def _make_eq(cls, attrs): - """ - Create __eq__ method for *cls* with *attrs*. - """ - attrs = [a for a in attrs if a.eq] - - unique_filename = _generate_unique_filename(cls, "eq") - lines = [ - "def __eq__(self, other):", - " if other.__class__ is not self.__class__:", - " return NotImplemented", - ] - - # We can't just do a big self.x = other.x and... clause due to - # irregularities like nan == nan is false but (nan,) == (nan,) is true. - globs = {} - if attrs: - lines.append(" return (") - others = [" ) == ("] - for a in attrs: - if a.eq_key: - cmp_name = "_%s_key" % (a.name,) - # Add the key function to the global namespace - # of the evaluated function. - globs[cmp_name] = a.eq_key - lines.append( - " %s(self.%s)," - % ( - cmp_name, - a.name, - ) - ) - others.append( - " %s(other.%s)," - % ( - cmp_name, - a.name, - ) - ) - else: - lines.append(" self.%s," % (a.name,)) - others.append(" other.%s," % (a.name,)) - - lines += others + [" )"] - else: - lines.append(" return True") - - script = "\n".join(lines) - - return _make_method("__eq__", script, unique_filename, globs) - - -def _make_order(cls, attrs): - """ - Create ordering methods for *cls* with *attrs*. - """ - attrs = [a for a in attrs if a.order] - - def attrs_to_tuple(obj): - """ - Save us some typing. - """ - return tuple( - key(value) if key else value - for value, key in ( - (getattr(obj, a.name), a.order_key) for a in attrs - ) - ) - - def __lt__(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) < attrs_to_tuple(other) - - return NotImplemented - - def __le__(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) <= attrs_to_tuple(other) - - return NotImplemented - - def __gt__(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) > attrs_to_tuple(other) - - return NotImplemented - - def __ge__(self, other): - """ - Automatically created by attrs. - """ - if other.__class__ is self.__class__: - return attrs_to_tuple(self) >= attrs_to_tuple(other) - - return NotImplemented - - return __lt__, __le__, __gt__, __ge__ - - -def _add_eq(cls, attrs=None): - """ - Add equality methods to *cls* with *attrs*. - """ - if attrs is None: - attrs = cls.__attrs_attrs__ - - cls.__eq__ = _make_eq(cls, attrs) - cls.__ne__ = _make_ne() - - return cls - - -if HAS_F_STRINGS: - - def _make_repr(attrs, ns, cls): - unique_filename = _generate_unique_filename(cls, "repr") - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, (repr if a.repr is True else a.repr), a.init) - for a in attrs - if a.repr is not False - ) - globs = { - name + "_repr": r - for name, r, _ in attr_names_with_reprs - if r != repr - } - globs["_compat"] = _compat - globs["AttributeError"] = AttributeError - globs["NOTHING"] = NOTHING - attribute_fragments = [] - for name, r, i in attr_names_with_reprs: - accessor = ( - "self." + name - if i - else 'getattr(self, "' + name + '", NOTHING)' - ) - fragment = ( - "%s={%s!r}" % (name, accessor) - if r == repr - else "%s={%s_repr(%s)}" % (name, name, accessor) - ) - attribute_fragments.append(fragment) - repr_fragment = ", ".join(attribute_fragments) - - if ns is None: - cls_name_fragment = ( - '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' - ) - else: - cls_name_fragment = ns + ".{self.__class__.__name__}" - - lines = [ - "def __repr__(self):", - " try:", - " already_repring = _compat.repr_context.already_repring", - " except AttributeError:", - " already_repring = {id(self),}", - " _compat.repr_context.already_repring = already_repring", - " else:", - " if id(self) in already_repring:", - " return '...'", - " else:", - " already_repring.add(id(self))", - " try:", - " return f'%s(%s)'" % (cls_name_fragment, repr_fragment), - " finally:", - " already_repring.remove(id(self))", - ] - - return _make_method( - "__repr__", "\n".join(lines), unique_filename, globs=globs - ) - -else: - - def _make_repr(attrs, ns, _): - """ - Make a repr method that includes relevant *attrs*, adding *ns* to the - full name. - """ - - # Figure out which attributes to include, and which function to use to - # format them. The a.repr value can be either bool or a custom - # callable. - attr_names_with_reprs = tuple( - (a.name, repr if a.repr is True else a.repr) - for a in attrs - if a.repr is not False - ) - - def __repr__(self): - """ - Automatically created by attrs. - """ - try: - already_repring = _compat.repr_context.already_repring - except AttributeError: - already_repring = set() - _compat.repr_context.already_repring = already_repring - - if id(self) in already_repring: - return "..." - real_cls = self.__class__ - if ns is None: - qualname = getattr(real_cls, "__qualname__", None) - if qualname is not None: # pragma: no cover - # This case only happens on Python 3.5 and 3.6. We exclude - # it from coverage, because we don't want to slow down our - # test suite by running them under coverage too for this - # one line. - class_name = qualname.rsplit(">.", 1)[-1] - else: - class_name = real_cls.__name__ - else: - class_name = ns + "." + real_cls.__name__ - - # Since 'self' remains on the stack (i.e.: strongly referenced) - # for the duration of this call, it's safe to depend on id(...) - # stability, and not need to track the instance and therefore - # worry about properties like weakref- or hash-ability. - already_repring.add(id(self)) - try: - result = [class_name, "("] - first = True - for name, attr_repr in attr_names_with_reprs: - if first: - first = False - else: - result.append(", ") - result.extend( - (name, "=", attr_repr(getattr(self, name, NOTHING))) - ) - return "".join(result) + ")" - finally: - already_repring.remove(id(self)) - - return __repr__ - - -def _add_repr(cls, ns=None, attrs=None): - """ - Add a repr method to *cls*. - """ - if attrs is None: - attrs = cls.__attrs_attrs__ - - cls.__repr__ = _make_repr(attrs, ns, cls) - return cls - - -def fields(cls): - """ - Return the tuple of ``attrs`` attributes for a class. - - The tuple also allows accessing the fields by their names (see below for - examples). - - :param type cls: Class to introspect. - - :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - :rtype: tuple (with name accessors) of `attrs.Attribute` - - .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields - by name. - """ - if not isclass(cls): - raise TypeError("Passed object must be a class.") - attrs = getattr(cls, "__attrs_attrs__", None) - if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return attrs - - -def fields_dict(cls): - """ - Return an ordered dictionary of ``attrs`` attributes for a class, whose - keys are the attribute names. - - :param type cls: Class to introspect. - - :raise TypeError: If *cls* is not a class. - :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` - class. - - :rtype: an ordered dict where keys are attribute names and values are - `attrs.Attribute`\\ s. This will be a `dict` if it's - naturally ordered like on Python 3.6+ or an - :class:`~collections.OrderedDict` otherwise. - - .. versionadded:: 18.1.0 - """ - if not isclass(cls): - raise TypeError("Passed object must be a class.") - attrs = getattr(cls, "__attrs_attrs__", None) - if attrs is None: - raise NotAnAttrsClassError( - "{cls!r} is not an attrs-decorated class.".format(cls=cls) - ) - return ordered_dict(((a.name, a) for a in attrs)) - - -def validate(inst): - """ - Validate all attributes on *inst* that have a validator. - - Leaves all exceptions through. - - :param inst: Instance of a class with ``attrs`` attributes. - """ - if _config._run_validators is False: - return - - for a in fields(inst.__class__): - v = a.validator - if v is not None: - v(inst, a, getattr(inst, a.name)) - - -def _is_slot_cls(cls): - return "__slots__" in cls.__dict__ - - -def _is_slot_attr(a_name, base_attr_map): - """ - Check if the attribute name comes from a slot class. - """ - return a_name in base_attr_map and _is_slot_cls(base_attr_map[a_name]) - - -def _make_init( - cls, - attrs, - pre_init, - post_init, - frozen, - slots, - cache_hash, - base_attr_map, - is_exc, - cls_on_setattr, - attrs_init, -): - has_cls_on_setattr = ( - cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP - ) - - if frozen and has_cls_on_setattr: - raise ValueError("Frozen classes can't use on_setattr.") - - needs_cached_setattr = cache_hash or frozen - filtered_attrs = [] - attr_dict = {} - for a in attrs: - if not a.init and a.default is NOTHING: - continue - - filtered_attrs.append(a) - attr_dict[a.name] = a - - if a.on_setattr is not None: - if frozen is True: - raise ValueError("Frozen classes can't use on_setattr.") - - needs_cached_setattr = True - elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: - needs_cached_setattr = True - - unique_filename = _generate_unique_filename(cls, "init") - - script, globs, annotations = _attrs_to_init_script( - filtered_attrs, - frozen, - slots, - pre_init, - post_init, - cache_hash, - base_attr_map, - is_exc, - needs_cached_setattr, - has_cls_on_setattr, - attrs_init, - ) - if cls.__module__ in sys.modules: - # This makes typing.get_type_hints(CLS.__init__) resolve string types. - globs.update(sys.modules[cls.__module__].__dict__) - - globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) - - if needs_cached_setattr: - # Save the lookup overhead in __init__ if we need to circumvent - # setattr hooks. - globs["_cached_setattr"] = _obj_setattr - - init = _make_method( - "__attrs_init__" if attrs_init else "__init__", - script, - unique_filename, - globs, - ) - init.__annotations__ = annotations - - return init - - -def _setattr(attr_name, value_var, has_on_setattr): - """ - Use the cached object.setattr to set *attr_name* to *value_var*. - """ - return "_setattr('%s', %s)" % (attr_name, value_var) - - -def _setattr_with_converter(attr_name, value_var, has_on_setattr): - """ - Use the cached object.setattr to set *attr_name* to *value_var*, but run - its converter first. - """ - return "_setattr('%s', %s(%s))" % ( - attr_name, - _init_converter_pat % (attr_name,), - value_var, - ) - - -def _assign(attr_name, value, has_on_setattr): - """ - Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise - relegate to _setattr. - """ - if has_on_setattr: - return _setattr(attr_name, value, True) - - return "self.%s = %s" % (attr_name, value) - - -def _assign_with_converter(attr_name, value_var, has_on_setattr): - """ - Unless *attr_name* has an on_setattr hook, use normal assignment after - conversion. Otherwise relegate to _setattr_with_converter. - """ - if has_on_setattr: - return _setattr_with_converter(attr_name, value_var, True) - - return "self.%s = %s(%s)" % ( - attr_name, - _init_converter_pat % (attr_name,), - value_var, - ) - - -if PY2: - - def _unpack_kw_only_py2(attr_name, default=None): - """ - Unpack *attr_name* from _kw_only dict. - """ - if default is not None: - arg_default = ", %s" % default - else: - arg_default = "" - return "%s = _kw_only.pop('%s'%s)" % ( - attr_name, - attr_name, - arg_default, - ) - - def _unpack_kw_only_lines_py2(kw_only_args): - """ - Unpack all *kw_only_args* from _kw_only dict and handle errors. - - Given a list of strings "{attr_name}" and "{attr_name}={default}" - generates list of lines of code that pop attrs from _kw_only dict and - raise TypeError similar to builtin if required attr is missing or - extra key is passed. - - >>> print("\n".join(_unpack_kw_only_lines_py2(["a", "b=42"]))) - try: - a = _kw_only.pop('a') - b = _kw_only.pop('b', 42) - except KeyError as _key_error: - raise TypeError( - ... - if _kw_only: - raise TypeError( - ... - """ - lines = ["try:"] - lines.extend( - " " + _unpack_kw_only_py2(*arg.split("=")) - for arg in kw_only_args - ) - lines += """\ -except KeyError as _key_error: - raise TypeError( - '__init__() missing required keyword-only argument: %s' % _key_error - ) -if _kw_only: - raise TypeError( - '__init__() got an unexpected keyword argument %r' - % next(iter(_kw_only)) - ) -""".split( - "\n" - ) - return lines - - -def _attrs_to_init_script( - attrs, - frozen, - slots, - pre_init, - post_init, - cache_hash, - base_attr_map, - is_exc, - needs_cached_setattr, - has_cls_on_setattr, - attrs_init, -): - """ - Return a script of an initializer for *attrs* and a dict of globals. - - The globals are expected by the generated script. - - If *frozen* is True, we cannot set the attributes directly so we use - a cached ``object.__setattr__``. - """ - lines = [] - if pre_init: - lines.append("self.__attrs_pre_init__()") - - if needs_cached_setattr: - lines.append( - # Circumvent the __setattr__ descriptor to save one lookup per - # assignment. - # Note _setattr will be used again below if cache_hash is True - "_setattr = _cached_setattr.__get__(self, self.__class__)" - ) - - if frozen is True: - if slots is True: - fmt_setter = _setattr - fmt_setter_with_converter = _setattr_with_converter - else: - # Dict frozen classes assign directly to __dict__. - # But only if the attribute doesn't come from an ancestor slot - # class. - # Note _inst_dict will be used again below if cache_hash is True - lines.append("_inst_dict = self.__dict__") - - def fmt_setter(attr_name, value_var, has_on_setattr): - if _is_slot_attr(attr_name, base_attr_map): - return _setattr(attr_name, value_var, has_on_setattr) - - return "_inst_dict['%s'] = %s" % (attr_name, value_var) - - def fmt_setter_with_converter( - attr_name, value_var, has_on_setattr - ): - if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): - return _setattr_with_converter( - attr_name, value_var, has_on_setattr - ) - - return "_inst_dict['%s'] = %s(%s)" % ( - attr_name, - _init_converter_pat % (attr_name,), - value_var, - ) - - else: - # Not frozen. - fmt_setter = _assign - fmt_setter_with_converter = _assign_with_converter - - args = [] - kw_only_args = [] - attrs_to_validate = [] - - # This is a dictionary of names to validator and converter callables. - # Injecting this into __init__ globals lets us avoid lookups. - names_for_globals = {} - annotations = {"return": None} - - for a in attrs: - if a.validator: - attrs_to_validate.append(a) - - attr_name = a.name - has_on_setattr = a.on_setattr is not None or ( - a.on_setattr is not setters.NO_OP and has_cls_on_setattr - ) - arg_name = a.name.lstrip("_") - - has_factory = isinstance(a.default, Factory) - if has_factory and a.default.takes_self: - maybe_self = "self" - else: - maybe_self = "" - - if a.init is False: - if has_factory: - init_factory_name = _init_factory_pat.format(a.name) - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, - init_factory_name + "(%s)" % (maybe_self,), - has_on_setattr, - ) - ) - conv_name = _init_converter_pat % (a.name,) - names_for_globals[conv_name] = a.converter - else: - lines.append( - fmt_setter( - attr_name, - init_factory_name + "(%s)" % (maybe_self,), - has_on_setattr, - ) - ) - names_for_globals[init_factory_name] = a.default.factory - else: - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, - "attr_dict['%s'].default" % (attr_name,), - has_on_setattr, - ) - ) - conv_name = _init_converter_pat % (a.name,) - names_for_globals[conv_name] = a.converter - else: - lines.append( - fmt_setter( - attr_name, - "attr_dict['%s'].default" % (attr_name,), - has_on_setattr, - ) - ) - elif a.default is not NOTHING and not has_factory: - arg = "%s=attr_dict['%s'].default" % (arg_name, attr_name) - if a.kw_only: - kw_only_args.append(arg) - else: - args.append(arg) - - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, arg_name, has_on_setattr - ) - ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter - else: - lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) - - elif has_factory: - arg = "%s=NOTHING" % (arg_name,) - if a.kw_only: - kw_only_args.append(arg) - else: - args.append(arg) - lines.append("if %s is not NOTHING:" % (arg_name,)) - - init_factory_name = _init_factory_pat.format(a.name) - if a.converter is not None: - lines.append( - " " - + fmt_setter_with_converter( - attr_name, arg_name, has_on_setattr - ) - ) - lines.append("else:") - lines.append( - " " - + fmt_setter_with_converter( - attr_name, - init_factory_name + "(" + maybe_self + ")", - has_on_setattr, - ) - ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter - else: - lines.append( - " " + fmt_setter(attr_name, arg_name, has_on_setattr) - ) - lines.append("else:") - lines.append( - " " - + fmt_setter( - attr_name, - init_factory_name + "(" + maybe_self + ")", - has_on_setattr, - ) - ) - names_for_globals[init_factory_name] = a.default.factory - else: - if a.kw_only: - kw_only_args.append(arg_name) - else: - args.append(arg_name) - - if a.converter is not None: - lines.append( - fmt_setter_with_converter( - attr_name, arg_name, has_on_setattr - ) - ) - names_for_globals[ - _init_converter_pat % (a.name,) - ] = a.converter - else: - lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) - - if a.init is True: - if a.type is not None and a.converter is None: - annotations[arg_name] = a.type - elif a.converter is not None and not PY2: - # Try to get the type from the converter. - sig = None - try: - sig = inspect.signature(a.converter) - except (ValueError, TypeError): # inspect failed - pass - if sig: - sig_params = list(sig.parameters.values()) - if ( - sig_params - and sig_params[0].annotation - is not inspect.Parameter.empty - ): - annotations[arg_name] = sig_params[0].annotation - - if attrs_to_validate: # we can skip this if there are no validators. - names_for_globals["_config"] = _config - lines.append("if _config._run_validators is True:") - for a in attrs_to_validate: - val_name = "__attr_validator_" + a.name - attr_name = "__attr_" + a.name - lines.append( - " %s(self, %s, self.%s)" % (val_name, attr_name, a.name) - ) - names_for_globals[val_name] = a.validator - names_for_globals[attr_name] = a - - if post_init: - lines.append("self.__attrs_post_init__()") - - # because this is set only after __attrs_post_init is called, a crash - # will result if post-init tries to access the hash code. This seemed - # preferable to setting this beforehand, in which case alteration to - # field values during post-init combined with post-init accessing the - # hash code would result in silent bugs. - if cache_hash: - if frozen: - if slots: - # if frozen and slots, then _setattr defined above - init_hash_cache = "_setattr('%s', %s)" - else: - # if frozen and not slots, then _inst_dict defined above - init_hash_cache = "_inst_dict['%s'] = %s" - else: - init_hash_cache = "self.%s = %s" - lines.append(init_hash_cache % (_hash_cache_field, "None")) - - # For exceptions we rely on BaseException.__init__ for proper - # initialization. - if is_exc: - vals = ",".join("self." + a.name for a in attrs if a.init) - - lines.append("BaseException.__init__(self, %s)" % (vals,)) - - args = ", ".join(args) - if kw_only_args: - if PY2: - lines = _unpack_kw_only_lines_py2(kw_only_args) + lines - - args += "%s**_kw_only" % (", " if args else "",) # leading comma - else: - args += "%s*, %s" % ( - ", " if args else "", # leading comma - ", ".join(kw_only_args), # kw_only args - ) - return ( - """\ -def {init_name}(self, {args}): - {lines} -""".format( - init_name=("__attrs_init__" if attrs_init else "__init__"), - args=args, - lines="\n ".join(lines) if lines else "pass", - ), - names_for_globals, - annotations, - ) - - -class Attribute(object): - """ - *Read-only* representation of an attribute. - - The class has *all* arguments of `attr.ib` (except for ``factory`` - which is only syntactic sugar for ``default=Factory(...)`` plus the - following: - - - ``name`` (`str`): The name of the attribute. - - ``inherited`` (`bool`): Whether or not that attribute has been inherited - from a base class. - - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The callables - that are used for comparing and ordering objects by this attribute, - respectively. These are set by passing a callable to `attr.ib`'s ``eq``, - ``order``, or ``cmp`` arguments. See also :ref:`comparison customization - `. - - Instances of this class are frequently used for introspection purposes - like: - - - `fields` returns a tuple of them. - - Validators get them passed as the first argument. - - The :ref:`field transformer ` hook receives a list of - them. - - .. versionadded:: 20.1.0 *inherited* - .. versionadded:: 20.1.0 *on_setattr* - .. versionchanged:: 20.2.0 *inherited* is not taken into account for - equality checks and hashing anymore. - .. versionadded:: 21.1.0 *eq_key* and *order_key* - - For the full version history of the fields, see `attr.ib`. - """ - - __slots__ = ( - "name", - "default", - "validator", - "repr", - "eq", - "eq_key", - "order", - "order_key", - "hash", - "init", - "metadata", - "type", - "converter", - "kw_only", - "inherited", - "on_setattr", - ) - - def __init__( - self, - name, - default, - validator, - repr, - cmp, # XXX: unused, remove along with other cmp code. - hash, - init, - inherited, - metadata=None, - type=None, - converter=None, - kw_only=False, - eq=None, - eq_key=None, - order=None, - order_key=None, - on_setattr=None, - ): - eq, eq_key, order, order_key = _determine_attrib_eq_order( - cmp, eq_key or eq, order_key or order, True - ) - - # Cache this descriptor here to speed things up later. - bound_setattr = _obj_setattr.__get__(self, Attribute) - - # Despite the big red warning, people *do* instantiate `Attribute` - # themselves. - bound_setattr("name", name) - bound_setattr("default", default) - bound_setattr("validator", validator) - bound_setattr("repr", repr) - bound_setattr("eq", eq) - bound_setattr("eq_key", eq_key) - bound_setattr("order", order) - bound_setattr("order_key", order_key) - bound_setattr("hash", hash) - bound_setattr("init", init) - bound_setattr("converter", converter) - bound_setattr( - "metadata", - ( - metadata_proxy(metadata) - if metadata - else _empty_metadata_singleton - ), - ) - bound_setattr("type", type) - bound_setattr("kw_only", kw_only) - bound_setattr("inherited", inherited) - bound_setattr("on_setattr", on_setattr) - - def __setattr__(self, name, value): - raise FrozenInstanceError() - - @classmethod - def from_counting_attr(cls, name, ca, type=None): - # type holds the annotated value. deal with conflicts: - if type is None: - type = ca.type - elif ca.type is not None: - raise ValueError( - "Type annotation and type argument cannot both be present" - ) - inst_dict = { - k: getattr(ca, k) - for k in Attribute.__slots__ - if k - not in ( - "name", - "validator", - "default", - "type", - "inherited", - ) # exclude methods and deprecated alias - } - return cls( - name=name, - validator=ca._validator, - default=ca._default, - type=type, - cmp=None, - inherited=False, - **inst_dict - ) - - @property - def cmp(self): - """ - Simulate the presence of a cmp attribute and warn. - """ - warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2) - - return self.eq and self.order - - # Don't use attr.evolve since fields(Attribute) doesn't work - def evolve(self, **changes): - """ - Copy *self* and apply *changes*. - - This works similarly to `attr.evolve` but that function does not work - with ``Attribute``. - - It is mainly meant to be used for `transform-fields`. - - .. versionadded:: 20.3.0 - """ - new = copy.copy(self) - - new._setattrs(changes.items()) - - return new - - # Don't use _add_pickle since fields(Attribute) doesn't work - def __getstate__(self): - """ - Play nice with pickle. - """ - return tuple( - getattr(self, name) if name != "metadata" else dict(self.metadata) - for name in self.__slots__ - ) - - def __setstate__(self, state): - """ - Play nice with pickle. - """ - self._setattrs(zip(self.__slots__, state)) - - def _setattrs(self, name_values_pairs): - bound_setattr = _obj_setattr.__get__(self, Attribute) - for name, value in name_values_pairs: - if name != "metadata": - bound_setattr(name, value) - else: - bound_setattr( - name, - metadata_proxy(value) - if value - else _empty_metadata_singleton, - ) - - -_a = [ - Attribute( - name=name, - default=NOTHING, - validator=None, - repr=True, - cmp=None, - eq=True, - order=False, - hash=(name != "metadata"), - init=True, - inherited=False, - ) - for name in Attribute.__slots__ -] - -Attribute = _add_hash( - _add_eq( - _add_repr(Attribute, attrs=_a), - attrs=[a for a in _a if a.name != "inherited"], - ), - attrs=[a for a in _a if a.hash and a.name != "inherited"], -) - - -class _CountingAttr(object): - """ - Intermediate representation of attributes that uses a counter to preserve - the order in which the attributes have been defined. - - *Internal* data structure of the attrs library. Running into is most - likely the result of a bug like a forgotten `@attr.s` decorator. - """ - - __slots__ = ( - "counter", - "_default", - "repr", - "eq", - "eq_key", - "order", - "order_key", - "hash", - "init", - "metadata", - "_validator", - "converter", - "type", - "kw_only", - "on_setattr", - ) - __attrs_attrs__ = tuple( - Attribute( - name=name, - default=NOTHING, - validator=None, - repr=True, - cmp=None, - hash=True, - init=True, - kw_only=False, - eq=True, - eq_key=None, - order=False, - order_key=None, - inherited=False, - on_setattr=None, - ) - for name in ( - "counter", - "_default", - "repr", - "eq", - "order", - "hash", - "init", - "on_setattr", - ) - ) + ( - Attribute( - name="metadata", - default=None, - validator=None, - repr=True, - cmp=None, - hash=False, - init=True, - kw_only=False, - eq=True, - eq_key=None, - order=False, - order_key=None, - inherited=False, - on_setattr=None, - ), - ) - cls_counter = 0 - - def __init__( - self, - default, - validator, - repr, - cmp, - hash, - init, - converter, - metadata, - type, - kw_only, - eq, - eq_key, - order, - order_key, - on_setattr, - ): - _CountingAttr.cls_counter += 1 - self.counter = _CountingAttr.cls_counter - self._default = default - self._validator = validator - self.converter = converter - self.repr = repr - self.eq = eq - self.eq_key = eq_key - self.order = order - self.order_key = order_key - self.hash = hash - self.init = init - self.metadata = metadata - self.type = type - self.kw_only = kw_only - self.on_setattr = on_setattr - - def validator(self, meth): - """ - Decorator that adds *meth* to the list of validators. - - Returns *meth* unchanged. - - .. versionadded:: 17.1.0 - """ - if self._validator is None: - self._validator = meth - else: - self._validator = and_(self._validator, meth) - return meth - - def default(self, meth): - """ - Decorator that allows to set the default for an attribute. - - Returns *meth* unchanged. - - :raises DefaultAlreadySetError: If default has been set before. - - .. versionadded:: 17.1.0 - """ - if self._default is not NOTHING: - raise DefaultAlreadySetError() - - self._default = Factory(meth, takes_self=True) - - return meth - - -_CountingAttr = _add_eq(_add_repr(_CountingAttr)) - - -class Factory(object): - """ - Stores a factory callable. - - If passed as the default value to `attrs.field`, the factory is used to - generate a new value. - - :param callable factory: A callable that takes either none or exactly one - mandatory positional argument depending on *takes_self*. - :param bool takes_self: Pass the partially initialized instance that is - being initialized as a positional argument. - - .. versionadded:: 17.1.0 *takes_self* - """ - - __slots__ = ("factory", "takes_self") - - def __init__(self, factory, takes_self=False): - """ - `Factory` is part of the default machinery so if we want a default - value here, we have to implement it ourselves. - """ - self.factory = factory - self.takes_self = takes_self - - def __getstate__(self): - """ - Play nice with pickle. - """ - return tuple(getattr(self, name) for name in self.__slots__) - - def __setstate__(self, state): - """ - Play nice with pickle. - """ - for name, value in zip(self.__slots__, state): - setattr(self, name, value) - - -_f = [ - Attribute( - name=name, - default=NOTHING, - validator=None, - repr=True, - cmp=None, - eq=True, - order=False, - hash=True, - init=True, - inherited=False, - ) - for name in Factory.__slots__ -] - -Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) - - -def make_class(name, attrs, bases=(object,), **attributes_arguments): - """ - A quick way to create a new class called *name* with *attrs*. - - :param str name: The name for the new class. - - :param attrs: A list of names or a dictionary of mappings of names to - attributes. - - If *attrs* is a list or an ordered dict (`dict` on Python 3.6+, - `collections.OrderedDict` otherwise), the order is deduced from - the order of the names or attributes inside *attrs*. Otherwise the - order of the definition of the attributes is used. - :type attrs: `list` or `dict` - - :param tuple bases: Classes that the new class will subclass. - - :param attributes_arguments: Passed unmodified to `attr.s`. - - :return: A new class with *attrs*. - :rtype: type - - .. versionadded:: 17.1.0 *bases* - .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. - """ - if isinstance(attrs, dict): - cls_dict = attrs - elif isinstance(attrs, (list, tuple)): - cls_dict = dict((a, attrib()) for a in attrs) - else: - raise TypeError("attrs argument must be a dict or a list.") - - pre_init = cls_dict.pop("__attrs_pre_init__", None) - post_init = cls_dict.pop("__attrs_post_init__", None) - user_init = cls_dict.pop("__init__", None) - - body = {} - if pre_init is not None: - body["__attrs_pre_init__"] = pre_init - if post_init is not None: - body["__attrs_post_init__"] = post_init - if user_init is not None: - body["__init__"] = user_init - - type_ = new_class(name, bases, {}, lambda ns: ns.update(body)) - - # For pickling to work, the __module__ variable needs to be set to the - # frame where the class is created. Bypass this step in environments where - # sys._getframe is not defined (Jython for example) or sys._getframe is not - # defined for arguments greater than 0 (IronPython). - try: - type_.__module__ = sys._getframe(1).f_globals.get( - "__name__", "__main__" - ) - except (AttributeError, ValueError): - pass - - # We do it here for proper warnings with meaningful stacklevel. - cmp = attributes_arguments.pop("cmp", None) - ( - attributes_arguments["eq"], - attributes_arguments["order"], - ) = _determine_attrs_eq_order( - cmp, - attributes_arguments.get("eq"), - attributes_arguments.get("order"), - True, - ) - - return _attrs(these=cls_dict, **attributes_arguments)(type_) - - -# These are required by within this module so we define them here and merely -# import into .validators / .converters. - - -@attrs(slots=True, hash=True) -class _AndValidator(object): - """ - Compose many validators to a single one. - """ - - _validators = attrib() - - def __call__(self, inst, attr, value): - for v in self._validators: - v(inst, attr, value) - - -def and_(*validators): - """ - A validator that composes multiple validators into one. - - When called on a value, it runs all wrapped validators. - - :param callables validators: Arbitrary number of validators. - - .. versionadded:: 17.1.0 - """ - vals = [] - for validator in validators: - vals.extend( - validator._validators - if isinstance(validator, _AndValidator) - else [validator] - ) - - return _AndValidator(tuple(vals)) - - -def pipe(*converters): - """ - A converter that composes multiple converters into one. - - When called on a value, it runs all wrapped converters, returning the - *last* value. - - Type annotations will be inferred from the wrapped converters', if - they have any. - - :param callables converters: Arbitrary number of converters. - - .. versionadded:: 20.1.0 - """ - - def pipe_converter(val): - for converter in converters: - val = converter(val) - - return val - - if not PY2: - if not converters: - # If the converter list is empty, pipe_converter is the identity. - A = typing.TypeVar("A") - pipe_converter.__annotations__ = {"val": A, "return": A} - else: - # Get parameter type. - sig = None - try: - sig = inspect.signature(converters[0]) - except (ValueError, TypeError): # inspect failed - pass - if sig: - params = list(sig.parameters.values()) - if ( - params - and params[0].annotation is not inspect.Parameter.empty - ): - pipe_converter.__annotations__["val"] = params[ - 0 - ].annotation - # Get return type. - sig = None - try: - sig = inspect.signature(converters[-1]) - except (ValueError, TypeError): # inspect failed - pass - if sig and sig.return_annotation is not inspect.Signature().empty: - pipe_converter.__annotations__[ - "return" - ] = sig.return_annotation - - return pipe_converter diff --git a/client/ayon_core/vendor/python/python_2/attr/_next_gen.py b/client/ayon_core/vendor/python/python_2/attr/_next_gen.py deleted file mode 100644 index 068253688c..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_next_gen.py +++ /dev/null @@ -1,216 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -These are Python 3.6+-only and keyword-only APIs that call `attr.s` and -`attr.ib` with different default values. -""" - - -from functools import partial - -from . import setters -from ._funcs import asdict as _asdict -from ._funcs import astuple as _astuple -from ._make import ( - NOTHING, - _frozen_setattrs, - _ng_default_on_setattr, - attrib, - attrs, -) -from .exceptions import UnannotatedAttributeError - - -def define( - maybe_cls=None, - *, - these=None, - repr=None, - hash=None, - init=None, - slots=True, - frozen=False, - weakref_slot=True, - str=False, - auto_attribs=None, - kw_only=False, - cache_hash=False, - auto_exc=True, - eq=None, - order=False, - auto_detect=True, - getstate_setstate=None, - on_setattr=None, - field_transformer=None, - match_args=True, -): - r""" - Define an ``attrs`` class. - - Differences to the classic `attr.s` that it uses underneath: - - - Automatically detect whether or not *auto_attribs* should be `True` - (c.f. *auto_attribs* parameter). - - If *frozen* is `False`, run converters and validators when setting an - attribute by default. - - *slots=True* (see :term:`slotted classes` for potentially surprising - behaviors) - - *auto_exc=True* - - *auto_detect=True* - - *order=False* - - *match_args=True* - - Some options that were only relevant on Python 2 or were kept around for - backwards-compatibility have been removed. - - Please note that these are all defaults and you can change them as you - wish. - - :param Optional[bool] auto_attribs: If set to `True` or `False`, it behaves - exactly like `attr.s`. If left `None`, `attr.s` will try to guess: - - 1. If any attributes are annotated and no unannotated `attrs.fields`\ s - are found, it assumes *auto_attribs=True*. - 2. Otherwise it assumes *auto_attribs=False* and tries to collect - `attrs.fields`\ s. - - For now, please refer to `attr.s` for the rest of the parameters. - - .. versionadded:: 20.1.0 - .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. - """ - - def do_it(cls, auto_attribs): - return attrs( - maybe_cls=cls, - these=these, - repr=repr, - hash=hash, - init=init, - slots=slots, - frozen=frozen, - weakref_slot=weakref_slot, - str=str, - auto_attribs=auto_attribs, - kw_only=kw_only, - cache_hash=cache_hash, - auto_exc=auto_exc, - eq=eq, - order=order, - auto_detect=auto_detect, - collect_by_mro=True, - getstate_setstate=getstate_setstate, - on_setattr=on_setattr, - field_transformer=field_transformer, - match_args=match_args, - ) - - def wrap(cls): - """ - Making this a wrapper ensures this code runs during class creation. - - We also ensure that frozen-ness of classes is inherited. - """ - nonlocal frozen, on_setattr - - had_on_setattr = on_setattr not in (None, setters.NO_OP) - - # By default, mutable classes convert & validate on setattr. - if frozen is False and on_setattr is None: - on_setattr = _ng_default_on_setattr - - # However, if we subclass a frozen class, we inherit the immutability - # and disable on_setattr. - for base_cls in cls.__bases__: - if base_cls.__setattr__ is _frozen_setattrs: - if had_on_setattr: - raise ValueError( - "Frozen classes can't use on_setattr " - "(frozen-ness was inherited)." - ) - - on_setattr = setters.NO_OP - break - - if auto_attribs is not None: - return do_it(cls, auto_attribs) - - try: - return do_it(cls, True) - except UnannotatedAttributeError: - return do_it(cls, False) - - # maybe_cls's type depends on the usage of the decorator. It's a class - # if it's used as `@attrs` but ``None`` if used as `@attrs()`. - if maybe_cls is None: - return wrap - else: - return wrap(maybe_cls) - - -mutable = define -frozen = partial(define, frozen=True, on_setattr=None) - - -def field( - *, - default=NOTHING, - validator=None, - repr=True, - hash=None, - init=True, - metadata=None, - converter=None, - factory=None, - kw_only=False, - eq=None, - order=None, - on_setattr=None, -): - """ - Identical to `attr.ib`, except keyword-only and with some arguments - removed. - - .. versionadded:: 20.1.0 - """ - return attrib( - default=default, - validator=validator, - repr=repr, - hash=hash, - init=init, - metadata=metadata, - converter=converter, - factory=factory, - kw_only=kw_only, - eq=eq, - order=order, - on_setattr=on_setattr, - ) - - -def asdict(inst, *, recurse=True, filter=None, value_serializer=None): - """ - Same as `attr.asdict`, except that collections types are always retained - and dict is always used as *dict_factory*. - - .. versionadded:: 21.3.0 - """ - return _asdict( - inst=inst, - recurse=recurse, - filter=filter, - value_serializer=value_serializer, - retain_collection_types=True, - ) - - -def astuple(inst, *, recurse=True, filter=None): - """ - Same as `attr.astuple`, except that collections types are always retained - and `tuple` is always used as the *tuple_factory*. - - .. versionadded:: 21.3.0 - """ - return _astuple( - inst=inst, recurse=recurse, filter=filter, retain_collection_types=True - ) diff --git a/client/ayon_core/vendor/python/python_2/attr/_version_info.py b/client/ayon_core/vendor/python/python_2/attr/_version_info.py deleted file mode 100644 index cdaeec37a1..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_version_info.py +++ /dev/null @@ -1,87 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - -from functools import total_ordering - -from ._funcs import astuple -from ._make import attrib, attrs - - -@total_ordering -@attrs(eq=False, order=False, slots=True, frozen=True) -class VersionInfo(object): - """ - A version object that can be compared to tuple of length 1--4: - - >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) - True - >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) - True - >>> vi = attr.VersionInfo(19, 2, 0, "final") - >>> vi < (19, 1, 1) - False - >>> vi < (19,) - False - >>> vi == (19, 2,) - True - >>> vi == (19, 2, 1) - False - - .. versionadded:: 19.2 - """ - - year = attrib(type=int) - minor = attrib(type=int) - micro = attrib(type=int) - releaselevel = attrib(type=str) - - @classmethod - def _from_version_string(cls, s): - """ - Parse *s* and return a _VersionInfo. - """ - v = s.split(".") - if len(v) == 3: - v.append("final") - - return cls( - year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] - ) - - def _ensure_tuple(self, other): - """ - Ensure *other* is a tuple of a valid length. - - Returns a possibly transformed *other* and ourselves as a tuple of - the same length as *other*. - """ - - if self.__class__ is other.__class__: - other = astuple(other) - - if not isinstance(other, tuple): - raise NotImplementedError - - if not (1 <= len(other) <= 4): - raise NotImplementedError - - return astuple(self)[: len(other)], other - - def __eq__(self, other): - try: - us, them = self._ensure_tuple(other) - except NotImplementedError: - return NotImplemented - - return us == them - - def __lt__(self, other): - try: - us, them = self._ensure_tuple(other) - except NotImplementedError: - return NotImplemented - - # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't - # have to do anything special with releaselevel for now. - return us < them diff --git a/client/ayon_core/vendor/python/python_2/attr/_version_info.pyi b/client/ayon_core/vendor/python/python_2/attr/_version_info.pyi deleted file mode 100644 index 45ced08633..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/_version_info.pyi +++ /dev/null @@ -1,9 +0,0 @@ -class VersionInfo: - @property - def year(self) -> int: ... - @property - def minor(self) -> int: ... - @property - def micro(self) -> int: ... - @property - def releaselevel(self) -> str: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/converters.py b/client/ayon_core/vendor/python/python_2/attr/converters.py deleted file mode 100644 index 1fb6c05d7b..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/converters.py +++ /dev/null @@ -1,155 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly useful converters. -""" - -from __future__ import absolute_import, division, print_function - -from ._compat import PY2 -from ._make import NOTHING, Factory, pipe - - -if not PY2: - import inspect - import typing - - -__all__ = [ - "default_if_none", - "optional", - "pipe", - "to_bool", -] - - -def optional(converter): - """ - A converter that allows an attribute to be optional. An optional attribute - is one which can be set to ``None``. - - Type annotations will be inferred from the wrapped converter's, if it - has any. - - :param callable converter: the converter that is used for non-``None`` - values. - - .. versionadded:: 17.1.0 - """ - - def optional_converter(val): - if val is None: - return None - return converter(val) - - if not PY2: - sig = None - try: - sig = inspect.signature(converter) - except (ValueError, TypeError): # inspect failed - pass - if sig: - params = list(sig.parameters.values()) - if params and params[0].annotation is not inspect.Parameter.empty: - optional_converter.__annotations__["val"] = typing.Optional[ - params[0].annotation - ] - if sig.return_annotation is not inspect.Signature.empty: - optional_converter.__annotations__["return"] = typing.Optional[ - sig.return_annotation - ] - - return optional_converter - - -def default_if_none(default=NOTHING, factory=None): - """ - A converter that allows to replace ``None`` values by *default* or the - result of *factory*. - - :param default: Value to be used if ``None`` is passed. Passing an instance - of `attrs.Factory` is supported, however the ``takes_self`` option - is *not*. - :param callable factory: A callable that takes no parameters whose result - is used if ``None`` is passed. - - :raises TypeError: If **neither** *default* or *factory* is passed. - :raises TypeError: If **both** *default* and *factory* are passed. - :raises ValueError: If an instance of `attrs.Factory` is passed with - ``takes_self=True``. - - .. versionadded:: 18.2.0 - """ - if default is NOTHING and factory is None: - raise TypeError("Must pass either `default` or `factory`.") - - if default is not NOTHING and factory is not None: - raise TypeError( - "Must pass either `default` or `factory` but not both." - ) - - if factory is not None: - default = Factory(factory) - - if isinstance(default, Factory): - if default.takes_self: - raise ValueError( - "`takes_self` is not supported by default_if_none." - ) - - def default_if_none_converter(val): - if val is not None: - return val - - return default.factory() - - else: - - def default_if_none_converter(val): - if val is not None: - return val - - return default - - return default_if_none_converter - - -def to_bool(val): - """ - Convert "boolean" strings (e.g., from env. vars.) to real booleans. - - Values mapping to :code:`True`: - - - :code:`True` - - :code:`"true"` / :code:`"t"` - - :code:`"yes"` / :code:`"y"` - - :code:`"on"` - - :code:`"1"` - - :code:`1` - - Values mapping to :code:`False`: - - - :code:`False` - - :code:`"false"` / :code:`"f"` - - :code:`"no"` / :code:`"n"` - - :code:`"off"` - - :code:`"0"` - - :code:`0` - - :raises ValueError: for any other value. - - .. versionadded:: 21.3.0 - """ - if isinstance(val, str): - val = val.lower() - truthy = {True, "true", "t", "yes", "y", "on", "1", 1} - falsy = {False, "false", "f", "no", "n", "off", "0", 0} - try: - if val in truthy: - return True - if val in falsy: - return False - except TypeError: - # Raised when "val" is not hashable (e.g., lists) - pass - raise ValueError("Cannot convert value to bool: {}".format(val)) diff --git a/client/ayon_core/vendor/python/python_2/attr/converters.pyi b/client/ayon_core/vendor/python/python_2/attr/converters.pyi deleted file mode 100644 index 0f58088a37..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/converters.pyi +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Callable, Optional, TypeVar, overload - -from . import _ConverterType - -_T = TypeVar("_T") - -def pipe(*validators: _ConverterType) -> _ConverterType: ... -def optional(converter: _ConverterType) -> _ConverterType: ... -@overload -def default_if_none(default: _T) -> _ConverterType: ... -@overload -def default_if_none(*, factory: Callable[[], _T]) -> _ConverterType: ... -def to_bool(val: str) -> bool: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/exceptions.py b/client/ayon_core/vendor/python/python_2/attr/exceptions.py deleted file mode 100644 index b2f1edc32a..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/exceptions.py +++ /dev/null @@ -1,94 +0,0 @@ -# SPDX-License-Identifier: MIT - -from __future__ import absolute_import, division, print_function - - -class FrozenError(AttributeError): - """ - A frozen/immutable instance or attribute have been attempted to be - modified. - - It mirrors the behavior of ``namedtuples`` by using the same error message - and subclassing `AttributeError`. - - .. versionadded:: 20.1.0 - """ - - msg = "can't set attribute" - args = [msg] - - -class FrozenInstanceError(FrozenError): - """ - A frozen instance has been attempted to be modified. - - .. versionadded:: 16.1.0 - """ - - -class FrozenAttributeError(FrozenError): - """ - A frozen attribute has been attempted to be modified. - - .. versionadded:: 20.1.0 - """ - - -class AttrsAttributeNotFoundError(ValueError): - """ - An ``attrs`` function couldn't find an attribute that the user asked for. - - .. versionadded:: 16.2.0 - """ - - -class NotAnAttrsClassError(ValueError): - """ - A non-``attrs`` class has been passed into an ``attrs`` function. - - .. versionadded:: 16.2.0 - """ - - -class DefaultAlreadySetError(RuntimeError): - """ - A default has been set using ``attr.ib()`` and is attempted to be reset - using the decorator. - - .. versionadded:: 17.1.0 - """ - - -class UnannotatedAttributeError(RuntimeError): - """ - A class with ``auto_attribs=True`` has an ``attr.ib()`` without a type - annotation. - - .. versionadded:: 17.3.0 - """ - - -class PythonTooOldError(RuntimeError): - """ - It was attempted to use an ``attrs`` feature that requires a newer Python - version. - - .. versionadded:: 18.2.0 - """ - - -class NotCallableError(TypeError): - """ - A ``attr.ib()`` requiring a callable has been set with a value - that is not callable. - - .. versionadded:: 19.2.0 - """ - - def __init__(self, msg, value): - super(TypeError, self).__init__(msg, value) - self.msg = msg - self.value = value - - def __str__(self): - return str(self.msg) diff --git a/client/ayon_core/vendor/python/python_2/attr/exceptions.pyi b/client/ayon_core/vendor/python/python_2/attr/exceptions.pyi deleted file mode 100644 index f2680118b4..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/exceptions.pyi +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Any - -class FrozenError(AttributeError): - msg: str = ... - -class FrozenInstanceError(FrozenError): ... -class FrozenAttributeError(FrozenError): ... -class AttrsAttributeNotFoundError(ValueError): ... -class NotAnAttrsClassError(ValueError): ... -class DefaultAlreadySetError(RuntimeError): ... -class UnannotatedAttributeError(RuntimeError): ... -class PythonTooOldError(RuntimeError): ... - -class NotCallableError(TypeError): - msg: str = ... - value: Any = ... - def __init__(self, msg: str, value: Any) -> None: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/filters.py b/client/ayon_core/vendor/python/python_2/attr/filters.py deleted file mode 100644 index a1978a8775..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/filters.py +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly useful filters for `attr.asdict`. -""" - -from __future__ import absolute_import, division, print_function - -from ._compat import isclass -from ._make import Attribute - - -def _split_what(what): - """ - Returns a tuple of `frozenset`s of classes and attributes. - """ - return ( - frozenset(cls for cls in what if isclass(cls)), - frozenset(cls for cls in what if isinstance(cls, Attribute)), - ) - - -def include(*what): - """ - Include *what*. - - :param what: What to include. - :type what: `list` of `type` or `attrs.Attribute`\\ s - - :rtype: `callable` - """ - cls, attrs = _split_what(what) - - def include_(attribute, value): - return value.__class__ in cls or attribute in attrs - - return include_ - - -def exclude(*what): - """ - Exclude *what*. - - :param what: What to exclude. - :type what: `list` of classes or `attrs.Attribute`\\ s. - - :rtype: `callable` - """ - cls, attrs = _split_what(what) - - def exclude_(attribute, value): - return value.__class__ not in cls and attribute not in attrs - - return exclude_ diff --git a/client/ayon_core/vendor/python/python_2/attr/filters.pyi b/client/ayon_core/vendor/python/python_2/attr/filters.pyi deleted file mode 100644 index 993866865e..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/filters.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Any, Union - -from . import Attribute, _FilterType - -def include(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... -def exclude(*what: Union[type, Attribute[Any]]) -> _FilterType[Any]: ... diff --git a/client/ayon_core/vendor/python/python_2/attr/py.typed b/client/ayon_core/vendor/python/python_2/attr/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/vendor/python/python_2/attr/setters.py b/client/ayon_core/vendor/python/python_2/attr/setters.py deleted file mode 100644 index b1cbb5d83e..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/setters.py +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly used hooks for on_setattr. -""" - -from __future__ import absolute_import, division, print_function - -from . import _config -from .exceptions import FrozenAttributeError - - -def pipe(*setters): - """ - Run all *setters* and return the return value of the last one. - - .. versionadded:: 20.1.0 - """ - - def wrapped_pipe(instance, attrib, new_value): - rv = new_value - - for setter in setters: - rv = setter(instance, attrib, rv) - - return rv - - return wrapped_pipe - - -def frozen(_, __, ___): - """ - Prevent an attribute to be modified. - - .. versionadded:: 20.1.0 - """ - raise FrozenAttributeError() - - -def validate(instance, attrib, new_value): - """ - Run *attrib*'s validator on *new_value* if it has one. - - .. versionadded:: 20.1.0 - """ - if _config._run_validators is False: - return new_value - - v = attrib.validator - if not v: - return new_value - - v(instance, attrib, new_value) - - return new_value - - -def convert(instance, attrib, new_value): - """ - Run *attrib*'s converter -- if it has one -- on *new_value* and return the - result. - - .. versionadded:: 20.1.0 - """ - c = attrib.converter - if c: - return c(new_value) - - return new_value - - -NO_OP = object() -""" -Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. - -Does not work in `pipe` or within lists. - -.. versionadded:: 20.1.0 -""" diff --git a/client/ayon_core/vendor/python/python_2/attr/setters.pyi b/client/ayon_core/vendor/python/python_2/attr/setters.pyi deleted file mode 100644 index 3f5603c2b0..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/setters.pyi +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Any, NewType, NoReturn, TypeVar, cast - -from . import Attribute, _OnSetAttrType - -_T = TypeVar("_T") - -def frozen( - instance: Any, attribute: Attribute[Any], new_value: Any -) -> NoReturn: ... -def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... -def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... - -# convert is allowed to return Any, because they can be chained using pipe. -def convert( - instance: Any, attribute: Attribute[Any], new_value: Any -) -> Any: ... - -_NoOpType = NewType("_NoOpType", object) -NO_OP: _NoOpType diff --git a/client/ayon_core/vendor/python/python_2/attr/validators.py b/client/ayon_core/vendor/python/python_2/attr/validators.py deleted file mode 100644 index 0b0c8342f2..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/validators.py +++ /dev/null @@ -1,561 +0,0 @@ -# SPDX-License-Identifier: MIT - -""" -Commonly useful validators. -""" - -from __future__ import absolute_import, division, print_function - -import operator -import re - -from contextlib import contextmanager - -from ._config import get_run_validators, set_run_validators -from ._make import _AndValidator, and_, attrib, attrs -from .exceptions import NotCallableError - - -try: - Pattern = re.Pattern -except AttributeError: # Python <3.7 lacks a Pattern type. - Pattern = type(re.compile("")) - - -__all__ = [ - "and_", - "deep_iterable", - "deep_mapping", - "disabled", - "ge", - "get_disabled", - "gt", - "in_", - "instance_of", - "is_callable", - "le", - "lt", - "matches_re", - "max_len", - "optional", - "provides", - "set_disabled", -] - - -def set_disabled(disabled): - """ - Globally disable or enable running validators. - - By default, they are run. - - :param disabled: If ``True``, disable running all validators. - :type disabled: bool - - .. warning:: - - This function is not thread-safe! - - .. versionadded:: 21.3.0 - """ - set_run_validators(not disabled) - - -def get_disabled(): - """ - Return a bool indicating whether validators are currently disabled or not. - - :return: ``True`` if validators are currently disabled. - :rtype: bool - - .. versionadded:: 21.3.0 - """ - return not get_run_validators() - - -@contextmanager -def disabled(): - """ - Context manager that disables running validators within its context. - - .. warning:: - - This context manager is not thread-safe! - - .. versionadded:: 21.3.0 - """ - set_run_validators(False) - try: - yield - finally: - set_run_validators(True) - - -@attrs(repr=False, slots=True, hash=True) -class _InstanceOfValidator(object): - type = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not isinstance(value, self.type): - raise TypeError( - "'{name}' must be {type!r} (got {value!r} that is a " - "{actual!r}).".format( - name=attr.name, - type=self.type, - actual=value.__class__, - value=value, - ), - attr, - self.type, - value, - ) - - def __repr__(self): - return "".format( - type=self.type - ) - - -def instance_of(type): - """ - A validator that raises a `TypeError` if the initializer is called - with a wrong type for this particular attribute (checks are performed using - `isinstance` therefore it's also valid to pass a tuple of types). - - :param type: The type to check for. - :type type: type or tuple of types - - :raises TypeError: With a human readable error message, the attribute - (of type `attrs.Attribute`), the expected type, and the value it - got. - """ - return _InstanceOfValidator(type) - - -@attrs(repr=False, frozen=True, slots=True) -class _MatchesReValidator(object): - pattern = attrib() - match_func = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not self.match_func(value): - raise ValueError( - "'{name}' must match regex {pattern!r}" - " ({value!r} doesn't)".format( - name=attr.name, pattern=self.pattern.pattern, value=value - ), - attr, - self.pattern, - value, - ) - - def __repr__(self): - return "".format( - pattern=self.pattern - ) - - -def matches_re(regex, flags=0, func=None): - r""" - A validator that raises `ValueError` if the initializer is called - with a string that doesn't match *regex*. - - :param regex: a regex string or precompiled pattern to match against - :param int flags: flags that will be passed to the underlying re function - (default 0) - :param callable func: which underlying `re` function to call (options - are `re.fullmatch`, `re.search`, `re.match`, default - is ``None`` which means either `re.fullmatch` or an emulation of - it on Python 2). For performance reasons, they won't be used directly - but on a pre-`re.compile`\ ed pattern. - - .. versionadded:: 19.2.0 - .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. - """ - fullmatch = getattr(re, "fullmatch", None) - valid_funcs = (fullmatch, None, re.search, re.match) - if func not in valid_funcs: - raise ValueError( - "'func' must be one of {}.".format( - ", ".join( - sorted( - e and e.__name__ or "None" for e in set(valid_funcs) - ) - ) - ) - ) - - if isinstance(regex, Pattern): - if flags: - raise TypeError( - "'flags' can only be used with a string pattern; " - "pass flags to re.compile() instead" - ) - pattern = regex - else: - pattern = re.compile(regex, flags) - - if func is re.match: - match_func = pattern.match - elif func is re.search: - match_func = pattern.search - elif fullmatch: - match_func = pattern.fullmatch - else: # Python 2 fullmatch emulation (https://bugs.python.org/issue16203) - pattern = re.compile( - r"(?:{})\Z".format(pattern.pattern), pattern.flags - ) - match_func = pattern.match - - return _MatchesReValidator(pattern, match_func) - - -@attrs(repr=False, slots=True, hash=True) -class _ProvidesValidator(object): - interface = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not self.interface.providedBy(value): - raise TypeError( - "'{name}' must provide {interface!r} which {value!r} " - "doesn't.".format( - name=attr.name, interface=self.interface, value=value - ), - attr, - self.interface, - value, - ) - - def __repr__(self): - return "".format( - interface=self.interface - ) - - -def provides(interface): - """ - A validator that raises a `TypeError` if the initializer is called - with an object that does not provide the requested *interface* (checks are - performed using ``interface.providedBy(value)`` (see `zope.interface - `_). - - :param interface: The interface to check for. - :type interface: ``zope.interface.Interface`` - - :raises TypeError: With a human readable error message, the attribute - (of type `attrs.Attribute`), the expected interface, and the - value it got. - """ - return _ProvidesValidator(interface) - - -@attrs(repr=False, slots=True, hash=True) -class _OptionalValidator(object): - validator = attrib() - - def __call__(self, inst, attr, value): - if value is None: - return - - self.validator(inst, attr, value) - - def __repr__(self): - return "".format( - what=repr(self.validator) - ) - - -def optional(validator): - """ - A validator that makes an attribute optional. An optional attribute is one - which can be set to ``None`` in addition to satisfying the requirements of - the sub-validator. - - :param validator: A validator (or a list of validators) that is used for - non-``None`` values. - :type validator: callable or `list` of callables. - - .. versionadded:: 15.1.0 - .. versionchanged:: 17.1.0 *validator* can be a list of validators. - """ - if isinstance(validator, list): - return _OptionalValidator(_AndValidator(validator)) - return _OptionalValidator(validator) - - -@attrs(repr=False, slots=True, hash=True) -class _InValidator(object): - options = attrib() - - def __call__(self, inst, attr, value): - try: - in_options = value in self.options - except TypeError: # e.g. `1 in "abc"` - in_options = False - - if not in_options: - raise ValueError( - "'{name}' must be in {options!r} (got {value!r})".format( - name=attr.name, options=self.options, value=value - ) - ) - - def __repr__(self): - return "".format( - options=self.options - ) - - -def in_(options): - """ - A validator that raises a `ValueError` if the initializer is called - with a value that does not belong in the options provided. The check is - performed using ``value in options``. - - :param options: Allowed options. - :type options: list, tuple, `enum.Enum`, ... - - :raises ValueError: With a human readable error message, the attribute (of - type `attrs.Attribute`), the expected options, and the value it - got. - - .. versionadded:: 17.1.0 - """ - return _InValidator(options) - - -@attrs(repr=False, slots=False, hash=True) -class _IsCallableValidator(object): - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not callable(value): - message = ( - "'{name}' must be callable " - "(got {value!r} that is a {actual!r})." - ) - raise NotCallableError( - msg=message.format( - name=attr.name, value=value, actual=value.__class__ - ), - value=value, - ) - - def __repr__(self): - return "" - - -def is_callable(): - """ - A validator that raises a `attr.exceptions.NotCallableError` if the - initializer is called with a value for this particular attribute - that is not callable. - - .. versionadded:: 19.1.0 - - :raises `attr.exceptions.NotCallableError`: With a human readable error - message containing the attribute (`attrs.Attribute`) name, - and the value it got. - """ - return _IsCallableValidator() - - -@attrs(repr=False, slots=True, hash=True) -class _DeepIterable(object): - member_validator = attrib(validator=is_callable()) - iterable_validator = attrib( - default=None, validator=optional(is_callable()) - ) - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if self.iterable_validator is not None: - self.iterable_validator(inst, attr, value) - - for member in value: - self.member_validator(inst, attr, member) - - def __repr__(self): - iterable_identifier = ( - "" - if self.iterable_validator is None - else " {iterable!r}".format(iterable=self.iterable_validator) - ) - return ( - "" - ).format( - iterable_identifier=iterable_identifier, - member=self.member_validator, - ) - - -def deep_iterable(member_validator, iterable_validator=None): - """ - A validator that performs deep validation of an iterable. - - :param member_validator: Validator to apply to iterable members - :param iterable_validator: Validator to apply to iterable itself - (optional) - - .. versionadded:: 19.1.0 - - :raises TypeError: if any sub-validators fail - """ - return _DeepIterable(member_validator, iterable_validator) - - -@attrs(repr=False, slots=True, hash=True) -class _DeepMapping(object): - key_validator = attrib(validator=is_callable()) - value_validator = attrib(validator=is_callable()) - mapping_validator = attrib(default=None, validator=optional(is_callable())) - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if self.mapping_validator is not None: - self.mapping_validator(inst, attr, value) - - for key in value: - self.key_validator(inst, attr, key) - self.value_validator(inst, attr, value[key]) - - def __repr__(self): - return ( - "" - ).format(key=self.key_validator, value=self.value_validator) - - -def deep_mapping(key_validator, value_validator, mapping_validator=None): - """ - A validator that performs deep validation of a dictionary. - - :param key_validator: Validator to apply to dictionary keys - :param value_validator: Validator to apply to dictionary values - :param mapping_validator: Validator to apply to top-level mapping - attribute (optional) - - .. versionadded:: 19.1.0 - - :raises TypeError: if any sub-validators fail - """ - return _DeepMapping(key_validator, value_validator, mapping_validator) - - -@attrs(repr=False, frozen=True, slots=True) -class _NumberValidator(object): - bound = attrib() - compare_op = attrib() - compare_func = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if not self.compare_func(value, self.bound): - raise ValueError( - "'{name}' must be {op} {bound}: {value}".format( - name=attr.name, - op=self.compare_op, - bound=self.bound, - value=value, - ) - ) - - def __repr__(self): - return "".format( - op=self.compare_op, bound=self.bound - ) - - -def lt(val): - """ - A validator that raises `ValueError` if the initializer is called - with a number larger or equal to *val*. - - :param val: Exclusive upper bound for values - - .. versionadded:: 21.3.0 - """ - return _NumberValidator(val, "<", operator.lt) - - -def le(val): - """ - A validator that raises `ValueError` if the initializer is called - with a number greater than *val*. - - :param val: Inclusive upper bound for values - - .. versionadded:: 21.3.0 - """ - return _NumberValidator(val, "<=", operator.le) - - -def ge(val): - """ - A validator that raises `ValueError` if the initializer is called - with a number smaller than *val*. - - :param val: Inclusive lower bound for values - - .. versionadded:: 21.3.0 - """ - return _NumberValidator(val, ">=", operator.ge) - - -def gt(val): - """ - A validator that raises `ValueError` if the initializer is called - with a number smaller or equal to *val*. - - :param val: Exclusive lower bound for values - - .. versionadded:: 21.3.0 - """ - return _NumberValidator(val, ">", operator.gt) - - -@attrs(repr=False, frozen=True, slots=True) -class _MaxLengthValidator(object): - max_length = attrib() - - def __call__(self, inst, attr, value): - """ - We use a callable class to be able to change the ``__repr__``. - """ - if len(value) > self.max_length: - raise ValueError( - "Length of '{name}' must be <= {max}: {len}".format( - name=attr.name, max=self.max_length, len=len(value) - ) - ) - - def __repr__(self): - return "".format(max=self.max_length) - - -def max_len(length): - """ - A validator that raises `ValueError` if the initializer is called - with a string or iterable that is longer than *length*. - - :param int length: Maximum length of the string or iterable - - .. versionadded:: 21.3.0 - """ - return _MaxLengthValidator(length) diff --git a/client/ayon_core/vendor/python/python_2/attr/validators.pyi b/client/ayon_core/vendor/python/python_2/attr/validators.pyi deleted file mode 100644 index 5e00b85433..0000000000 --- a/client/ayon_core/vendor/python/python_2/attr/validators.pyi +++ /dev/null @@ -1,78 +0,0 @@ -from typing import ( - Any, - AnyStr, - Callable, - Container, - ContextManager, - Iterable, - List, - Mapping, - Match, - Optional, - Pattern, - Tuple, - Type, - TypeVar, - Union, - overload, -) - -from . import _ValidatorType - -_T = TypeVar("_T") -_T1 = TypeVar("_T1") -_T2 = TypeVar("_T2") -_T3 = TypeVar("_T3") -_I = TypeVar("_I", bound=Iterable) -_K = TypeVar("_K") -_V = TypeVar("_V") -_M = TypeVar("_M", bound=Mapping) - -def set_disabled(run: bool) -> None: ... -def get_disabled() -> bool: ... -def disabled() -> ContextManager[None]: ... - -# To be more precise on instance_of use some overloads. -# If there are more than 3 items in the tuple then we fall back to Any -@overload -def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ... -@overload -def instance_of(type: Tuple[Type[_T]]) -> _ValidatorType[_T]: ... -@overload -def instance_of( - type: Tuple[Type[_T1], Type[_T2]] -) -> _ValidatorType[Union[_T1, _T2]]: ... -@overload -def instance_of( - type: Tuple[Type[_T1], Type[_T2], Type[_T3]] -) -> _ValidatorType[Union[_T1, _T2, _T3]]: ... -@overload -def instance_of(type: Tuple[type, ...]) -> _ValidatorType[Any]: ... -def provides(interface: Any) -> _ValidatorType[Any]: ... -def optional( - validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] -) -> _ValidatorType[Optional[_T]]: ... -def in_(options: Container[_T]) -> _ValidatorType[_T]: ... -def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... -def matches_re( - regex: Union[Pattern[AnyStr], AnyStr], - flags: int = ..., - func: Optional[ - Callable[[AnyStr, AnyStr, int], Optional[Match[AnyStr]]] - ] = ..., -) -> _ValidatorType[AnyStr]: ... -def deep_iterable( - member_validator: _ValidatorType[_T], - iterable_validator: Optional[_ValidatorType[_I]] = ..., -) -> _ValidatorType[_I]: ... -def deep_mapping( - key_validator: _ValidatorType[_K], - value_validator: _ValidatorType[_V], - mapping_validator: Optional[_ValidatorType[_M]] = ..., -) -> _ValidatorType[_M]: ... -def is_callable() -> _ValidatorType[_T]: ... -def lt(val: _T) -> _ValidatorType[_T]: ... -def le(val: _T) -> _ValidatorType[_T]: ... -def ge(val: _T) -> _ValidatorType[_T]: ... -def gt(val: _T) -> _ValidatorType[_T]: ... -def max_len(length: int) -> _ValidatorType[_T]: ... diff --git a/client/ayon_core/vendor/python/python_2/attrs/__init__.py b/client/ayon_core/vendor/python/python_2/attrs/__init__.py deleted file mode 100644 index a704b8b56b..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr import ( - NOTHING, - Attribute, - Factory, - __author__, - __copyright__, - __description__, - __doc__, - __email__, - __license__, - __title__, - __url__, - __version__, - __version_info__, - assoc, - cmp_using, - define, - evolve, - field, - fields, - fields_dict, - frozen, - has, - make_class, - mutable, - resolve_types, - validate, -) -from attr._next_gen import asdict, astuple - -from . import converters, exceptions, filters, setters, validators - - -__all__ = [ - "__author__", - "__copyright__", - "__description__", - "__doc__", - "__email__", - "__license__", - "__title__", - "__url__", - "__version__", - "__version_info__", - "asdict", - "assoc", - "astuple", - "Attribute", - "cmp_using", - "converters", - "define", - "evolve", - "exceptions", - "Factory", - "field", - "fields_dict", - "fields", - "filters", - "frozen", - "has", - "make_class", - "mutable", - "NOTHING", - "resolve_types", - "setters", - "validate", - "validators", -] diff --git a/client/ayon_core/vendor/python/python_2/attrs/__init__.pyi b/client/ayon_core/vendor/python/python_2/attrs/__init__.pyi deleted file mode 100644 index 7426fa5ddb..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/__init__.pyi +++ /dev/null @@ -1,63 +0,0 @@ -from typing import ( - Any, - Callable, - Dict, - Mapping, - Optional, - Sequence, - Tuple, - Type, -) - -# Because we need to type our own stuff, we have to make everything from -# attr explicitly public too. -from attr import __author__ as __author__ -from attr import __copyright__ as __copyright__ -from attr import __description__ as __description__ -from attr import __email__ as __email__ -from attr import __license__ as __license__ -from attr import __title__ as __title__ -from attr import __url__ as __url__ -from attr import __version__ as __version__ -from attr import __version_info__ as __version_info__ -from attr import _FilterType -from attr import assoc as assoc -from attr import Attribute as Attribute -from attr import define as define -from attr import evolve as evolve -from attr import Factory as Factory -from attr import exceptions as exceptions -from attr import field as field -from attr import fields as fields -from attr import fields_dict as fields_dict -from attr import frozen as frozen -from attr import has as has -from attr import make_class as make_class -from attr import mutable as mutable -from attr import NOTHING as NOTHING -from attr import resolve_types as resolve_types -from attr import setters as setters -from attr import validate as validate -from attr import validators as validators - -# TODO: see definition of attr.asdict/astuple -def asdict( - inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - dict_factory: Type[Mapping[Any, Any]] = ..., - retain_collection_types: bool = ..., - value_serializer: Optional[ - Callable[[type, Attribute[Any], Any], Any] - ] = ..., - tuple_keys: bool = ..., -) -> Dict[str, Any]: ... - -# TODO: add support for returning NamedTuple from the mypy plugin -def astuple( - inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType[Any]] = ..., - tuple_factory: Type[Sequence[Any]] = ..., - retain_collection_types: bool = ..., -) -> Tuple[Any, ...]: ... diff --git a/client/ayon_core/vendor/python/python_2/attrs/converters.py b/client/ayon_core/vendor/python/python_2/attrs/converters.py deleted file mode 100644 index edfa8d3c16..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/converters.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.converters import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/attrs/exceptions.py b/client/ayon_core/vendor/python/python_2/attrs/exceptions.py deleted file mode 100644 index bd9efed202..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/exceptions.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.exceptions import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/attrs/filters.py b/client/ayon_core/vendor/python/python_2/attrs/filters.py deleted file mode 100644 index 52959005b0..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/filters.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.filters import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/attrs/py.typed b/client/ayon_core/vendor/python/python_2/attrs/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/vendor/python/python_2/attrs/setters.py b/client/ayon_core/vendor/python/python_2/attrs/setters.py deleted file mode 100644 index 9b50770804..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/setters.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.setters import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/attrs/validators.py b/client/ayon_core/vendor/python/python_2/attrs/validators.py deleted file mode 100644 index ab2c9b3024..0000000000 --- a/client/ayon_core/vendor/python/python_2/attrs/validators.py +++ /dev/null @@ -1,3 +0,0 @@ -# SPDX-License-Identifier: MIT - -from attr.validators import * # noqa diff --git a/client/ayon_core/vendor/python/python_2/backports/__init__.py b/client/ayon_core/vendor/python/python_2/backports/__init__.py deleted file mode 100644 index 69e3be50da..0000000000 --- a/client/ayon_core/vendor/python/python_2/backports/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/client/ayon_core/vendor/python/python_2/backports/configparser/__init__.py b/client/ayon_core/vendor/python/python_2/backports/configparser/__init__.py deleted file mode 100644 index 06d7a0855f..0000000000 --- a/client/ayon_core/vendor/python/python_2/backports/configparser/__init__.py +++ /dev/null @@ -1,1390 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""Configuration file parser. - -A configuration file consists of sections, lead by a "[section]" header, -and followed by "name: value" entries, with continuations and such in -the style of RFC 822. - -Intrinsic defaults can be specified by passing them into the -ConfigParser constructor as a dictionary. - -class: - -ConfigParser -- responsible for parsing a list of - configuration files, and managing the parsed database. - - methods: - - __init__(defaults=None, dict_type=_default_dict, allow_no_value=False, - delimiters=('=', ':'), comment_prefixes=('#', ';'), - inline_comment_prefixes=None, strict=True, - empty_lines_in_values=True, default_section='DEFAULT', - interpolation=, converters=): - Create the parser. When `defaults' is given, it is initialized into the - dictionary or intrinsic defaults. The keys must be strings, the values - must be appropriate for %()s string interpolation. - - When `dict_type' is given, it will be used to create the dictionary - objects for the list of sections, for the options within a section, and - for the default values. - - When `delimiters' is given, it will be used as the set of substrings - that divide keys from values. - - When `comment_prefixes' is given, it will be used as the set of - substrings that prefix comments in empty lines. Comments can be - indented. - - When `inline_comment_prefixes' is given, it will be used as the set of - substrings that prefix comments in non-empty lines. - - When `strict` is True, the parser won't allow for any section or option - duplicates while reading from a single source (file, string or - dictionary). Default is True. - - When `empty_lines_in_values' is False (default: True), each empty line - marks the end of an option. Otherwise, internal empty lines of - a multiline option are kept as part of the value. - - When `allow_no_value' is True (default: False), options without - values are accepted; the value presented for these is None. - - sections() - Return all the configuration section names, sans DEFAULT. - - has_section(section) - Return whether the given section exists. - - has_option(section, option) - Return whether the given option exists in the given section. - - options(section) - Return list of configuration options for the named section. - - read(filenames, encoding=None) - Read and parse the list of named configuration files, given by - name. A single filename is also allowed. Non-existing files - are ignored. Return list of successfully read files. - - read_file(f, filename=None) - Read and parse one configuration file, given as a file object. - The filename defaults to f.name; it is only used in error - messages (if f has no `name' attribute, the string `' is used). - - read_string(string) - Read configuration from a given string. - - read_dict(dictionary) - Read configuration from a dictionary. Keys are section names, - values are dictionaries with keys and values that should be present - in the section. If the used dictionary type preserves order, sections - and their keys will be added in order. Values are automatically - converted to strings. - - get(section, option, raw=False, vars=None, fallback=_UNSET) - Return a string value for the named option. All % interpolations are - expanded in the return values, based on the defaults passed into the - constructor and the DEFAULT section. Additional substitutions may be - provided using the `vars' argument, which must be a dictionary whose - contents override any pre-existing defaults. If `option' is a key in - `vars', the value from `vars' is used. - - getint(section, options, raw=False, vars=None, fallback=_UNSET) - Like get(), but convert value to an integer. - - getfloat(section, options, raw=False, vars=None, fallback=_UNSET) - Like get(), but convert value to a float. - - getboolean(section, options, raw=False, vars=None, fallback=_UNSET) - Like get(), but convert value to a boolean (currently case - insensitively defined as 0, false, no, off for False, and 1, true, - yes, on for True). Returns False or True. - - items(section=_UNSET, raw=False, vars=None) - If section is given, return a list of tuples with (name, value) for - each option in the section. Otherwise, return a list of tuples with - (section_name, section_proxy) for each section, including DEFAULTSECT. - - remove_section(section) - Remove the given file section and all its options. - - remove_option(section, option) - Remove the given option from the given section. - - set(section, option, value) - Set the given option. - - write(fp, space_around_delimiters=True) - Write the configuration state in .ini format. If - `space_around_delimiters' is True (the default), delimiters - between keys and values are surrounded by spaces. -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - -from collections import MutableMapping -import functools -import io -import itertools -import re -import sys -import warnings - -from backports.configparser.helpers import OrderedDict as _default_dict -from backports.configparser.helpers import ChainMap as _ChainMap -from backports.configparser.helpers import from_none, open, str, PY2 - -__all__ = ["NoSectionError", "DuplicateOptionError", "DuplicateSectionError", - "NoOptionError", "InterpolationError", "InterpolationDepthError", - "InterpolationMissingOptionError", "InterpolationSyntaxError", - "ParsingError", "MissingSectionHeaderError", - "ConfigParser", "SafeConfigParser", "RawConfigParser", - "Interpolation", "BasicInterpolation", "ExtendedInterpolation", - "LegacyInterpolation", "SectionProxy", "ConverterMapping", - "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH"] - -DEFAULTSECT = "DEFAULT" - -MAX_INTERPOLATION_DEPTH = 10 - - -# exception classes -class Error(Exception): - """Base class for ConfigParser exceptions.""" - - def __init__(self, msg=''): - self.message = msg - Exception.__init__(self, msg) - - def __repr__(self): - return self.message - - __str__ = __repr__ - - -class NoSectionError(Error): - """Raised when no section matches a requested option.""" - - def __init__(self, section): - Error.__init__(self, 'No section: %r' % (section,)) - self.section = section - self.args = (section, ) - - -class DuplicateSectionError(Error): - """Raised when a section is repeated in an input source. - - Possible repetitions that raise this exception are: multiple creation - using the API or in strict parsers when a section is found more than once - in a single input file, string or dictionary. - """ - - def __init__(self, section, source=None, lineno=None): - msg = [repr(section), " already exists"] - if source is not None: - message = ["While reading from ", repr(source)] - if lineno is not None: - message.append(" [line {0:2d}]".format(lineno)) - message.append(": section ") - message.extend(msg) - msg = message - else: - msg.insert(0, "Section ") - Error.__init__(self, "".join(msg)) - self.section = section - self.source = source - self.lineno = lineno - self.args = (section, source, lineno) - - -class DuplicateOptionError(Error): - """Raised by strict parsers when an option is repeated in an input source. - - Current implementation raises this exception only when an option is found - more than once in a single file, string or dictionary. - """ - - def __init__(self, section, option, source=None, lineno=None): - msg = [repr(option), " in section ", repr(section), - " already exists"] - if source is not None: - message = ["While reading from ", repr(source)] - if lineno is not None: - message.append(" [line {0:2d}]".format(lineno)) - message.append(": option ") - message.extend(msg) - msg = message - else: - msg.insert(0, "Option ") - Error.__init__(self, "".join(msg)) - self.section = section - self.option = option - self.source = source - self.lineno = lineno - self.args = (section, option, source, lineno) - - -class NoOptionError(Error): - """A requested option was not found.""" - - def __init__(self, option, section): - Error.__init__(self, "No option %r in section: %r" % - (option, section)) - self.option = option - self.section = section - self.args = (option, section) - - -class InterpolationError(Error): - """Base class for interpolation-related exceptions.""" - - def __init__(self, option, section, msg): - Error.__init__(self, msg) - self.option = option - self.section = section - self.args = (option, section, msg) - - -class InterpolationMissingOptionError(InterpolationError): - """A string substitution required a setting which was not available.""" - - def __init__(self, option, section, rawval, reference): - msg = ("Bad value substitution: option {0!r} in section {1!r} contains " - "an interpolation key {2!r} which is not a valid option name. " - "Raw value: {3!r}".format(option, section, reference, rawval)) - InterpolationError.__init__(self, option, section, msg) - self.reference = reference - self.args = (option, section, rawval, reference) - - -class InterpolationSyntaxError(InterpolationError): - """Raised when the source text contains invalid syntax. - - Current implementation raises this exception when the source text into - which substitutions are made does not conform to the required syntax. - """ - - -class InterpolationDepthError(InterpolationError): - """Raised when substitutions are nested too deeply.""" - - def __init__(self, option, section, rawval): - msg = ("Recursion limit exceeded in value substitution: option {0!r} " - "in section {1!r} contains an interpolation key which " - "cannot be substituted in {2} steps. Raw value: {3!r}" - "".format(option, section, MAX_INTERPOLATION_DEPTH, - rawval)) - InterpolationError.__init__(self, option, section, msg) - self.args = (option, section, rawval) - - -class ParsingError(Error): - """Raised when a configuration file does not follow legal syntax.""" - - def __init__(self, source=None, filename=None): - # Exactly one of `source'/`filename' arguments has to be given. - # `filename' kept for compatibility. - if filename and source: - raise ValueError("Cannot specify both `filename' and `source'. " - "Use `source'.") - elif not filename and not source: - raise ValueError("Required argument `source' not given.") - elif filename: - source = filename - Error.__init__(self, 'Source contains parsing errors: %r' % source) - self.source = source - self.errors = [] - self.args = (source, ) - - @property - def filename(self): - """Deprecated, use `source'.""" - warnings.warn( - "The 'filename' attribute will be removed in future versions. " - "Use 'source' instead.", - DeprecationWarning, stacklevel=2 - ) - return self.source - - @filename.setter - def filename(self, value): - """Deprecated, user `source'.""" - warnings.warn( - "The 'filename' attribute will be removed in future versions. " - "Use 'source' instead.", - DeprecationWarning, stacklevel=2 - ) - self.source = value - - def append(self, lineno, line): - self.errors.append((lineno, line)) - self.message += '\n\t[line %2d]: %s' % (lineno, line) - - -class MissingSectionHeaderError(ParsingError): - """Raised when a key-value pair is found before any section header.""" - - def __init__(self, filename, lineno, line): - Error.__init__( - self, - 'File contains no section headers.\nfile: %r, line: %d\n%r' % - (filename, lineno, line)) - self.source = filename - self.lineno = lineno - self.line = line - self.args = (filename, lineno, line) - - -# Used in parser getters to indicate the default behaviour when a specific -# option is not found it to raise an exception. Created to enable `None' as -# a valid fallback value. -_UNSET = object() - - -class Interpolation(object): - """Dummy interpolation that passes the value through with no changes.""" - - def before_get(self, parser, section, option, value, defaults): - return value - - def before_set(self, parser, section, option, value): - return value - - def before_read(self, parser, section, option, value): - return value - - def before_write(self, parser, section, option, value): - return value - - -class BasicInterpolation(Interpolation): - """Interpolation as implemented in the classic ConfigParser. - - The option values can contain format strings which refer to other values in - the same section, or values in the special default section. - - For example: - - something: %(dir)s/whatever - - would resolve the "%(dir)s" to the value of dir. All reference - expansions are done late, on demand. If a user needs to use a bare % in - a configuration file, she can escape it by writing %%. Other % usage - is considered a user error and raises `InterpolationSyntaxError'.""" - - _KEYCRE = re.compile(r"%\(([^)]+)\)s") - - def before_get(self, parser, section, option, value, defaults): - L = [] - self._interpolate_some(parser, option, L, value, section, defaults, 1) - return ''.join(L) - - def before_set(self, parser, section, option, value): - tmp_value = value.replace('%%', '') # escaped percent signs - tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax - if '%' in tmp_value: - raise ValueError("invalid interpolation syntax in %r at " - "position %d" % (value, tmp_value.find('%'))) - return value - - def _interpolate_some(self, parser, option, accum, rest, section, map, - depth): - rawval = parser.get(section, option, raw=True, fallback=rest) - if depth > MAX_INTERPOLATION_DEPTH: - raise InterpolationDepthError(option, section, rawval) - while rest: - p = rest.find("%") - if p < 0: - accum.append(rest) - return - if p > 0: - accum.append(rest[:p]) - rest = rest[p:] - # p is no longer used - c = rest[1:2] - if c == "%": - accum.append("%") - rest = rest[2:] - elif c == "(": - m = self._KEYCRE.match(rest) - if m is None: - raise InterpolationSyntaxError(option, section, - "bad interpolation variable reference %r" % rest) - var = parser.optionxform(m.group(1)) - rest = rest[m.end():] - try: - v = map[var] - except KeyError: - raise from_none(InterpolationMissingOptionError( - option, section, rawval, var)) - if "%" in v: - self._interpolate_some(parser, option, accum, v, - section, map, depth + 1) - else: - accum.append(v) - else: - raise InterpolationSyntaxError( - option, section, - "'%%' must be followed by '%%' or '(', " - "found: %r" % (rest,)) - - -class ExtendedInterpolation(Interpolation): - """Advanced variant of interpolation, supports the syntax used by - `zc.buildout'. Enables interpolation between sections.""" - - _KEYCRE = re.compile(r"\$\{([^}]+)\}") - - def before_get(self, parser, section, option, value, defaults): - L = [] - self._interpolate_some(parser, option, L, value, section, defaults, 1) - return ''.join(L) - - def before_set(self, parser, section, option, value): - tmp_value = value.replace('$$', '') # escaped dollar signs - tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax - if '$' in tmp_value: - raise ValueError("invalid interpolation syntax in %r at " - "position %d" % (value, tmp_value.find('$'))) - return value - - def _interpolate_some(self, parser, option, accum, rest, section, map, - depth): - rawval = parser.get(section, option, raw=True, fallback=rest) - if depth > MAX_INTERPOLATION_DEPTH: - raise InterpolationDepthError(option, section, rawval) - while rest: - p = rest.find("$") - if p < 0: - accum.append(rest) - return - if p > 0: - accum.append(rest[:p]) - rest = rest[p:] - # p is no longer used - c = rest[1:2] - if c == "$": - accum.append("$") - rest = rest[2:] - elif c == "{": - m = self._KEYCRE.match(rest) - if m is None: - raise InterpolationSyntaxError(option, section, - "bad interpolation variable reference %r" % rest) - path = m.group(1).split(':') - rest = rest[m.end():] - sect = section - opt = option - try: - if len(path) == 1: - opt = parser.optionxform(path[0]) - v = map[opt] - elif len(path) == 2: - sect = path[0] - opt = parser.optionxform(path[1]) - v = parser.get(sect, opt, raw=True) - else: - raise InterpolationSyntaxError( - option, section, - "More than one ':' found: %r" % (rest,)) - except (KeyError, NoSectionError, NoOptionError): - raise from_none(InterpolationMissingOptionError( - option, section, rawval, ":".join(path))) - if "$" in v: - self._interpolate_some(parser, opt, accum, v, sect, - dict(parser.items(sect, raw=True)), - depth + 1) - else: - accum.append(v) - else: - raise InterpolationSyntaxError( - option, section, - "'$' must be followed by '$' or '{', " - "found: %r" % (rest,)) - - -class LegacyInterpolation(Interpolation): - """Deprecated interpolation used in old versions of ConfigParser. - Use BasicInterpolation or ExtendedInterpolation instead.""" - - _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") - - def before_get(self, parser, section, option, value, vars): - rawval = value - depth = MAX_INTERPOLATION_DEPTH - while depth: # Loop through this until it's done - depth -= 1 - if value and "%(" in value: - replace = functools.partial(self._interpolation_replace, - parser=parser) - value = self._KEYCRE.sub(replace, value) - try: - value = value % vars - except KeyError as e: - raise from_none(InterpolationMissingOptionError( - option, section, rawval, e.args[0])) - else: - break - if value and "%(" in value: - raise InterpolationDepthError(option, section, rawval) - return value - - def before_set(self, parser, section, option, value): - return value - - @staticmethod - def _interpolation_replace(match, parser): - s = match.group(1) - if s is None: - return match.group() - else: - return "%%(%s)s" % parser.optionxform(s) - - -class RawConfigParser(MutableMapping): - """ConfigParser that does not do interpolation.""" - - # Regular expressions for parsing section headers and options - _SECT_TMPL = r""" - \[ # [ - (?P
[^]]+) # very permissive! - \] # ] - """ - _OPT_TMPL = r""" - (?P