From 569ce30b9672c77e6d553a03098160c0c13e166c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:38:44 +0100 Subject: [PATCH 01/24] pass all required arguments to FileDefItem --- client/ayon_core/lib/attribute_definitions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 34956fd33f..789c878d40 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -966,7 +966,9 @@ class FileDef(AbstractAttrDef): FileDefItem.from_dict(default) elif isinstance(default, str): - default = FileDefItem.from_paths([default.strip()])[0] + default = FileDefItem.from_paths( + [default.strip()], allow_sequences + )[0] else: raise TypeError(( @@ -1044,7 +1046,9 @@ class FileDef(AbstractAttrDef): pass if string_paths: - file_items = FileDefItem.from_paths(string_paths) + file_items = FileDefItem.from_paths( + string_paths, self.allow_sequences + ) dict_items.extend([ file_item.to_dict() for file_item in file_items From 521d8ed9ec87df5487480ebbfdfac5b31f7dfab4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:28:07 +0100 Subject: [PATCH 02/24] move register functions below classes --- client/ayon_core/lib/attribute_definitions.py | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 34956fd33f..e4e998189d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -15,67 +15,6 @@ import clique _attr_defs_by_type = {} -def register_attr_def_class(cls): - """Register attribute definition. - - Currently registered definitions are used to deserialize data to objects. - - Attrs: - cls (AbstractAttrDef): Non-abstract class to be registered with unique - 'type' attribute. - - Raises: - KeyError: When type was already registered. - """ - - if cls.type in _attr_defs_by_type: - raise KeyError("Type \"{}\" was already registered".format(cls.type)) - _attr_defs_by_type[cls.type] = cls - - -def get_attributes_keys(attribute_definitions): - """Collect keys from list of attribute definitions. - - Args: - attribute_definitions (List[AbstractAttrDef]): Objects of attribute - definitions. - - Returns: - Set[str]: Keys that will be created using passed attribute definitions. - """ - - keys = set() - if not attribute_definitions: - return keys - - for attribute_def in attribute_definitions: - if not isinstance(attribute_def, UIDef): - keys.add(attribute_def.key) - return keys - - -def get_default_values(attribute_definitions): - """Receive default values for attribute definitions. - - Args: - attribute_definitions (List[AbstractAttrDef]): Attribute definitions - for which default values should be collected. - - Returns: - Dict[str, Any]: Default values for passed attribute definitions. - """ - - output = {} - if not attribute_definitions: - return output - - for attr_def in attribute_definitions: - # Skip UI definitions - if not isinstance(attr_def, UIDef): - output[attr_def.key] = attr_def.default - return output - - class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. @@ -1062,6 +1001,67 @@ class FileDef(AbstractAttrDef): return [] +def register_attr_def_class(cls): + """Register attribute definition. + + Currently registered definitions are used to deserialize data to objects. + + Attrs: + cls (AbstractAttrDef): Non-abstract class to be registered with unique + 'type' attribute. + + Raises: + KeyError: When type was already registered. + """ + + if cls.type in _attr_defs_by_type: + raise KeyError("Type \"{}\" was already registered".format(cls.type)) + _attr_defs_by_type[cls.type] = cls + + +def get_attributes_keys(attribute_definitions): + """Collect keys from list of attribute definitions. + + Args: + attribute_definitions (List[AbstractAttrDef]): Objects of attribute + definitions. + + Returns: + Set[str]: Keys that will be created using passed attribute definitions. + """ + + keys = set() + if not attribute_definitions: + return keys + + for attribute_def in attribute_definitions: + if not isinstance(attribute_def, UIDef): + keys.add(attribute_def.key) + return keys + + +def get_default_values(attribute_definitions): + """Receive default values for attribute definitions. + + Args: + attribute_definitions (List[AbstractAttrDef]): Attribute definitions + for which default values should be collected. + + Returns: + Dict[str, Any]: Default values for passed attribute definitions. + """ + + output = {} + if not attribute_definitions: + return output + + for attr_def in attribute_definitions: + # Skip UI definitions + if not isinstance(attr_def, UIDef): + output[attr_def.key] = attr_def.default + return output + + def serialize_attr_def(attr_def): """Serialize attribute definition to data. From 9d629eca2fd87903afd28d2998c4522f8be67fd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:30:55 +0100 Subject: [PATCH 03/24] added helper type definitions --- client/ayon_core/lib/attribute_definitions.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e4e998189d..76abe5fe4d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -6,14 +6,33 @@ import json import copy import warnings from abc import ABCMeta, abstractmethod -from typing import Any, Optional +import typing +from typing import Any, Optional, List, TypedDict import clique +if typing.TYPE_CHECKING: + from typing import Union # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} +# Type hint helpers +IntFloatType = "Union[int, float]" + + +class EnumItemDict(TypedDict): + label: str + value: Any + + +class FileDefItemDict(TypedDict): + directory: str + filenames: List[str] + frames: Optional[List[int]] + template: Optional[str] + is_sequence: Optional[bool] + class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. From 443ebf8523adbbee9112c5f4aa26980f6c3122ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:43:46 +0100 Subject: [PATCH 04/24] added most of typehints --- client/ayon_core/lib/attribute_definitions.py | 177 ++++++++++++------ 1 file changed, 117 insertions(+), 60 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 76abe5fe4d..82c7ab9cb1 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -7,12 +7,14 @@ import copy import warnings from abc import ABCMeta, abstractmethod import typing -from typing import Any, Optional, List, TypedDict +from typing import ( + Any, Optional, List, Set, Dict, Iterable, TypedDict, TypeVar, +) import clique if typing.TYPE_CHECKING: - from typing import Union + from typing import Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} @@ -51,8 +53,12 @@ class AbstractAttrDefMeta(ABCMeta): def _convert_reversed_attr( - main_value, depr_value, main_label, depr_label, default -): + main_value: Any, + depr_value: Any, + main_label: str, + depr_label: str, + default: Any, +) -> Any: if main_value is not None and depr_value is not None: if main_value == depr_value: print( @@ -141,7 +147,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def id(self) -> str: return self._id - def clone(self): + def clone(self) -> "Self": data = self.serialize() data.pop("type") return self.deserialize(data) @@ -214,7 +220,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): pass @abstractmethod - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: """Convert value to a valid one. Convert passed value to a valid type. Use default if value can't be @@ -223,7 +229,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): pass - def serialize(self): + def serialize(self) -> Dict[str, Any]: """Serialize object to data so it's possible to recreate it. Returns: @@ -246,7 +252,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return data @classmethod - def deserialize(cls, data): + def deserialize(cls, data: Dict[str, Any]) -> "Self": """Recreate object from data. Data can be received using 'serialize' method. @@ -257,7 +263,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return cls(**data) - def _def_type_compare(self, other: "AbstractAttrDef") -> bool: + def _def_type_compare(self, other: "Self") -> bool: return True @@ -268,13 +274,19 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): class UIDef(AbstractAttrDef): is_value_def = False - def __init__(self, key=None, default=None, *args, **kwargs): + def __init__( + self, + key: Optional[str] = None, + default: Optional[Any] = None, + *args, + **kwargs + ): super().__init__(key, default, *args, **kwargs) def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -305,14 +317,14 @@ class UnknownDef(AbstractAttrDef): type = "unknown" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[Any] = None, **kwargs): kwargs["default"] = default super().__init__(key, **kwargs) def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -327,7 +339,7 @@ class HiddenDef(AbstractAttrDef): type = "hidden" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[Any] = None, **kwargs): kwargs["default"] = default kwargs["visible"] = False super().__init__(key, **kwargs) @@ -335,7 +347,7 @@ class HiddenDef(AbstractAttrDef): def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -360,7 +372,12 @@ class NumberDef(AbstractAttrDef): ] def __init__( - self, key, minimum=None, maximum=None, decimals=None, default=None, + self, + key: str, + minimum: Optional[IntFloatType] = None, + maximum: Optional[IntFloatType] = None, + decimals: Optional[int] = None, + default: Optional[IntFloatType] = None, **kwargs ): minimum = 0 if minimum is None else minimum @@ -386,9 +403,9 @@ class NumberDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.minimum = minimum - self.maximum = maximum - self.decimals = 0 if decimals is None else decimals + self.minimum: IntFloatType = minimum + self.maximum: IntFloatType = maximum + self.decimals: int = 0 if decimals is None else decimals def is_value_valid(self, value: Any) -> bool: if self.decimals == 0: @@ -400,7 +417,7 @@ class NumberDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value(self, value: Any) -> IntFloatType: if isinstance(value, str): try: value = float(value) @@ -444,7 +461,12 @@ class TextDef(AbstractAttrDef): ] def __init__( - self, key, multiline=None, regex=None, placeholder=None, default=None, + self, + key: str, + multiline: Optional[bool] = None, + regex: Optional[str] = None, + placeholder: Optional[str] = None, + default: Optional[str] = None, **kwargs ): if default is None: @@ -463,9 +485,9 @@ class TextDef(AbstractAttrDef): if isinstance(regex, str): regex = re.compile(regex) - self.multiline = multiline - self.placeholder = placeholder - self.regex = regex + self.multiline: bool = multiline + self.placeholder: Optional[str] = placeholder + self.regex: Optional["Pattern"] = regex def is_value_valid(self, value: Any) -> bool: if not isinstance(value, str): @@ -474,12 +496,12 @@ class TextDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value(self, value: Any) -> str: if isinstance(value, str): return value return self.default - def serialize(self): + def serialize(self) -> Dict[str, Any]: data = super().serialize() regex = None if self.regex is not None: @@ -503,8 +525,9 @@ class EnumDef(AbstractAttrDef): is enabled. Args: - items (Union[list[str], list[dict[str, Any]]): Items definition that - can be converted using 'prepare_enum_items'. + key (str): Key under which value is stored. + items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): Items + definition that can be converted using 'prepare_enum_items'. default (Optional[Any]): Default value. Must be one key(value) from passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. @@ -514,7 +537,12 @@ class EnumDef(AbstractAttrDef): type = "enum" def __init__( - self, key, items, default=None, multiselection=False, **kwargs + self, + key: str, + items: "Union[Dict[Any, str], List[Any], List[EnumItemDict]]", + default: "Union[str, List[Any]]" = None, + multiselection: Optional[bool] = False, + **kwargs ): if not items: raise ValueError(( @@ -525,6 +553,9 @@ class EnumDef(AbstractAttrDef): items = self.prepare_enum_items(items) item_values = [item["value"] for item in items] item_values_set = set(item_values) + if multiselection is None: + multiselection = False + if multiselection: if default is None: default = [] @@ -535,9 +566,9 @@ class EnumDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.items = items - self._item_values = item_values_set - self.multiselection = multiselection + self.items: List[EnumItemDict] = items + self._item_values: Set[Any] = item_values_set + self.multiselection: bool = multiselection def convert_value(self, value): if not self.multiselection: @@ -567,7 +598,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items): + def prepare_enum_items(items) -> List[EnumItemDict]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -583,13 +614,13 @@ class EnumDef(AbstractAttrDef): ``` Args: - items (Union[Dict[str, Any], List[Any], List[Dict[str, Any]]): The + items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): The items to convert. Returns: - List[Dict[str, Any]]: Unified structure of items. - """ + List[EnumItemDict]: Unified structure of items. + """ output = [] if isinstance(items, dict): for value, label in items.items(): @@ -644,7 +675,7 @@ class BoolDef(AbstractAttrDef): type = "bool" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[bool] = None, **kwargs): if default is None: default = False super().__init__(key, default=default, **kwargs) @@ -652,7 +683,7 @@ class BoolDef(AbstractAttrDef): def is_value_valid(self, value: Any) -> bool: return isinstance(value, bool) - def convert_value(self, value): + def convert_value(self, value: Any) -> bool: if isinstance(value, bool): return value return self.default @@ -660,7 +691,11 @@ class BoolDef(AbstractAttrDef): class FileDefItem: def __init__( - self, directory, filenames, frames=None, template=None + self, + directory: str, + filenames: List[str], + frames: Optional[List[int]] = None, + template: Optional[str] = None, ): self.directory = directory @@ -689,7 +724,7 @@ class FileDefItem: ) @property - def label(self): + def label(self) -> Optional[str]: if self.is_empty: return None @@ -732,7 +767,7 @@ class FileDefItem: filename_template, ",".join(ranges) ) - def split_sequence(self): + def split_sequence(self) -> List["Self"]: if not self.is_sequence: raise ValueError("Cannot split single file item") @@ -743,7 +778,7 @@ class FileDefItem: return self.from_paths(paths, False) @property - def ext(self): + def ext(self) -> Optional[str]: if self.is_empty: return None _, ext = os.path.splitext(self.filenames[0]) @@ -752,14 +787,14 @@ class FileDefItem: return None @property - def lower_ext(self): + def lower_ext(self) -> Optional[str]: ext = self.ext if ext is not None: return ext.lower() return ext @property - def is_dir(self): + def is_dir(self) -> bool: if self.is_empty: return False @@ -768,10 +803,15 @@ class FileDefItem: return False return True - def set_directory(self, directory): + def set_directory(self, directory: str): self.directory = directory - def set_filenames(self, filenames, frames=None, template=None): + def set_filenames( + self, + filenames: List[str], + frames: Optional[List[int]] = None, + template: Optional[str] = None, + ): if frames is None: frames = [] is_sequence = False @@ -788,11 +828,15 @@ class FileDefItem: self.is_sequence = is_sequence @classmethod - def create_empty_item(cls): + def create_empty_item(cls) -> "Self": return cls("", "") @classmethod - def from_value(cls, value, allow_sequences): + def from_value( + cls, + value: "Union[List[FileDefItemDict], FileDefItemDict]", + allow_sequences: bool, + ) -> List["Self"]: """Convert passed value to FileDefItem objects. Returns: @@ -830,7 +874,7 @@ class FileDefItem: return output @classmethod - def from_dict(cls, data): + def from_dict(cls, data: FileDefItemDict) -> "Self": return cls( data["directory"], data["filenames"], @@ -839,7 +883,11 @@ class FileDefItem: ) @classmethod - def from_paths(cls, paths, allow_sequences): + def from_paths( + cls, + paths: List[str], + allow_sequences: bool, + ) -> List["Self"]: filenames_by_dir = collections.defaultdict(list) for path in paths: normalized = os.path.normpath(path) @@ -868,7 +916,7 @@ class FileDefItem: return output - def to_dict(self): + def to_dict(self) -> FileDefItemDict: output = { "is_sequence": self.is_sequence, "directory": self.directory, @@ -906,8 +954,15 @@ class FileDef(AbstractAttrDef): ] def __init__( - self, key, single_item=True, folders=None, extensions=None, - allow_sequences=True, extensions_label=None, default=None, **kwargs + self, + key: str, + single_item: Optional[bool] = True, + folders: Optional[bool] = None, + extensions: Optional[Iterable[str]] = None, + allow_sequences: Optional[bool] = True, + extensions_label: Optional[str] = None, + default: Optional["Union[FileDefItemDict, List[str]]"] = None, + **kwargs ): if folders is None and extensions is None: folders = True @@ -943,14 +998,14 @@ class FileDef(AbstractAttrDef): if is_label_horizontal is None: kwargs["is_label_horizontal"] = False - self.single_item = single_item - self.folders = folders - self.extensions = set(extensions) - self.allow_sequences = allow_sequences - self.extensions_label = extensions_label + self.single_item: bool = single_item + self.folders: bool = folders + self.extensions: Set[str] = set(extensions) + self.allow_sequences: bool = allow_sequences + self.extensions_label: Optional[str] = extensions_label super().__init__(key, default=default, **kwargs) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if not super().__eq__(other): return False @@ -984,7 +1039,9 @@ class FileDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value( + self, value: Any + ) -> "Union[FileDefItemDict, List[FileDefItemDict]]": if isinstance(value, (str, dict)): value = [value] From 586d29f219f76572ffea9c431fe9f197cd0a2907 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:44:32 +0100 Subject: [PATCH 05/24] define 'EnumItemsInputType' for EnumDef input items --- client/ayon_core/lib/attribute_definitions.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 82c7ab9cb1..bf47b7617b 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -8,13 +8,22 @@ import warnings from abc import ABCMeta, abstractmethod import typing from typing import ( - Any, Optional, List, Set, Dict, Iterable, TypedDict, TypeVar, + Any, + Optional, + Tuple, + List, + Set, + Dict, + Iterable, + TypedDict, + TypeVar, ) import clique if typing.TYPE_CHECKING: from typing import Self, Union, Pattern + # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} @@ -28,6 +37,9 @@ class EnumItemDict(TypedDict): value: Any +EnumItemsInputType = "Union[Dict[Any, str], List[Tuple[Any, str]], List[Any], List[EnumItemDict]]" # noqa: E501 + + class FileDefItemDict(TypedDict): directory: str filenames: List[str] @@ -526,8 +538,8 @@ class EnumDef(AbstractAttrDef): Args: key (str): Key under which value is stored. - items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): Items - definition that can be converted using 'prepare_enum_items'. + items (EnumItemsInputType): Items definition that can be converted + using 'prepare_enum_items'. default (Optional[Any]): Default value. Must be one key(value) from passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. @@ -539,7 +551,7 @@ class EnumDef(AbstractAttrDef): def __init__( self, key: str, - items: "Union[Dict[Any, str], List[Any], List[EnumItemDict]]", + items: EnumItemsInputType, default: "Union[str, List[Any]]" = None, multiselection: Optional[bool] = False, **kwargs @@ -598,7 +610,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items) -> List[EnumItemDict]: + def prepare_enum_items(items: EnumItemsInputType) -> List[EnumItemDict]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -614,8 +626,7 @@ class EnumDef(AbstractAttrDef): ``` Args: - items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): The - items to convert. + items (EnumItemsInputType): The items to convert. Returns: List[EnumItemDict]: Unified structure of items. From b2a9277267a36fbbad093a5fc94b5f33e286de0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:55:48 +0100 Subject: [PATCH 06/24] define 'AttrDefType' --- client/ayon_core/lib/attribute_definitions.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index bf47b7617b..836d6c7463 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -279,6 +279,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return True +AttrDefType = TypeVar("AttrDefType", bound=AbstractAttrDef) + # ----------------------------------------- # UI attribute definitions won't hold value # ----------------------------------------- @@ -1088,13 +1090,13 @@ class FileDef(AbstractAttrDef): return [] -def register_attr_def_class(cls): +def register_attr_def_class(cls: AttrDefType): """Register attribute definition. Currently registered definitions are used to deserialize data to objects. Attrs: - cls (AbstractAttrDef): Non-abstract class to be registered with unique + cls (AttrDefType): Non-abstract class to be registered with unique 'type' attribute. Raises: @@ -1106,11 +1108,13 @@ def register_attr_def_class(cls): _attr_defs_by_type[cls.type] = cls -def get_attributes_keys(attribute_definitions): +def get_attributes_keys( + attribute_definitions: List[AttrDefType] +) -> Set[str]: """Collect keys from list of attribute definitions. Args: - attribute_definitions (List[AbstractAttrDef]): Objects of attribute + attribute_definitions (List[AttrDefType]): Objects of attribute definitions. Returns: @@ -1127,11 +1131,13 @@ def get_attributes_keys(attribute_definitions): return keys -def get_default_values(attribute_definitions): +def get_default_values( + attribute_definitions: List[AttrDefType] +) -> Dict[str, Any]: """Receive default values for attribute definitions. Args: - attribute_definitions (List[AbstractAttrDef]): Attribute definitions + attribute_definitions (List[AttrDefType]): Attribute definitions for which default values should be collected. Returns: @@ -1149,11 +1155,11 @@ def get_default_values(attribute_definitions): return output -def serialize_attr_def(attr_def): +def serialize_attr_def(attr_def: AttrDefType) -> Dict[str, Any]: """Serialize attribute definition to data. Args: - attr_def (AbstractAttrDef): Attribute definition to serialize. + attr_def (AttrDefType): Attribute definition to serialize. Returns: Dict[str, Any]: Serialized data. @@ -1162,11 +1168,13 @@ def serialize_attr_def(attr_def): return attr_def.serialize() -def serialize_attr_defs(attr_defs): +def serialize_attr_defs( + attr_defs: List[AttrDefType] +) -> List[Dict[str, Any]]: """Serialize attribute definitions to data. Args: - attr_defs (List[AbstractAttrDef]): Attribute definitions to serialize. + attr_defs (List[AttrDefType]): Attribute definitions to serialize. Returns: List[Dict[str, Any]]: Serialized data. @@ -1178,7 +1186,7 @@ def serialize_attr_defs(attr_defs): ] -def deserialize_attr_def(attr_def_data): +def deserialize_attr_def(attr_def_data: Dict[str, Any]) -> AttrDefType: """Deserialize attribute definition from data. Args: @@ -1191,7 +1199,9 @@ def deserialize_attr_def(attr_def_data): return cls.deserialize(attr_def_data) -def deserialize_attr_defs(attr_defs_data): +def deserialize_attr_defs( + attr_defs_data: List[Dict[str, Any]] +) -> List[AttrDefType]: """Deserialize attribute definitions. Args: From 341dc04cabd6982b636689cd63e83f3a9f0b3a5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:55:59 +0100 Subject: [PATCH 07/24] change formatting of docstrings --- client/ayon_core/lib/attribute_definitions.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 836d6c7463..9e1a92b18e 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -52,8 +52,8 @@ class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. Each object of `AbstractAttrDef` must have defined 'key' attribute. - """ + """ def __call__(cls, *args, **kwargs): obj = super(AbstractAttrDefMeta, cls).__call__(*args, **kwargs) init_class = getattr(obj, "__init__class__", None) @@ -116,8 +116,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): enabled (Optional[bool]): Item is enabled (for UI purposes). hidden (Optional[bool]): DEPRECATED: Use 'visible' instead. disabled (Optional[bool]): DEPRECATED: Use 'enabled' instead. - """ + """ type_attributes = [] is_value_def = True @@ -227,8 +227,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Returns: str: Type of attribute definition. - """ + """ pass @abstractmethod @@ -237,8 +237,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Convert passed value to a valid type. Use default if value can't be converted. - """ + """ pass def serialize(self) -> Dict[str, Any]: @@ -247,8 +247,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Returns: Dict[str, Any]: Serialized object that can be passed to 'deserialize' method. - """ + """ data = { "type": self.type, "key": self.key, @@ -327,8 +327,8 @@ class UnknownDef(AbstractAttrDef): This attribute can be used to keep existing data unchanged but does not have known definition of type. - """ + """ type = "unknown" def __init__(self, key: str, default: Optional[Any] = None, **kwargs): @@ -349,8 +349,8 @@ class HiddenDef(AbstractAttrDef): to other attributes (e.g. in multi-page UIs). Keep in mind the value should be possible to parse by json parser. - """ + """ type = "hidden" def __init__(self, key: str, default: Optional[Any] = None, **kwargs): @@ -376,8 +376,8 @@ class NumberDef(AbstractAttrDef): maximum(int, float): Maximum possible value. decimals(int): Maximum decimal points of value. default(int, float): Default value for conversion. - """ + """ type = "number" type_attributes = [ "minimum", @@ -466,8 +466,8 @@ class TextDef(AbstractAttrDef): regex(str, re.Pattern): Regex validation. placeholder(str): UI placeholder for attribute. default(str, None): Default value. Empty string used when not defined. - """ + """ type = "text" type_attributes = [ "multiline", @@ -546,8 +546,8 @@ class EnumDef(AbstractAttrDef): passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. Output is list of selected items. - """ + """ type = "enum" def __init__( @@ -684,8 +684,8 @@ class BoolDef(AbstractAttrDef): Args: default(bool): Default value. Set to `False` if not defined. - """ + """ type = "bool" def __init__(self, key: str, default: Optional[bool] = None, **kwargs): @@ -854,8 +854,8 @@ class FileDefItem: Returns: list: Created FileDefItem objects. - """ + """ # Convert single item to iterable if not isinstance(value, (list, tuple, set)): value = [value] @@ -1101,8 +1101,8 @@ def register_attr_def_class(cls: AttrDefType): Raises: KeyError: When type was already registered. - """ + """ if cls.type in _attr_defs_by_type: raise KeyError("Type \"{}\" was already registered".format(cls.type)) _attr_defs_by_type[cls.type] = cls @@ -1119,8 +1119,8 @@ def get_attributes_keys( Returns: Set[str]: Keys that will be created using passed attribute definitions. - """ + """ keys = set() if not attribute_definitions: return keys @@ -1142,8 +1142,8 @@ def get_default_values( Returns: Dict[str, Any]: Default values for passed attribute definitions. - """ + """ output = {} if not attribute_definitions: return output @@ -1163,8 +1163,8 @@ def serialize_attr_def(attr_def: AttrDefType) -> Dict[str, Any]: Returns: Dict[str, Any]: Serialized data. - """ + """ return attr_def.serialize() @@ -1178,8 +1178,8 @@ def serialize_attr_defs( Returns: List[Dict[str, Any]]: Serialized data. - """ + """ return [ serialize_attr_def(attr_def) for attr_def in attr_defs @@ -1192,8 +1192,8 @@ def deserialize_attr_def(attr_def_data: Dict[str, Any]) -> AttrDefType: Args: attr_def_data (Dict[str, Any]): Attribute definition data to deserialize. - """ + """ attr_type = attr_def_data.pop("type") cls = _attr_defs_by_type[attr_type] return cls.deserialize(attr_def_data) @@ -1206,8 +1206,8 @@ def deserialize_attr_defs( Args: List[Dict[str, Any]]: List of attribute definitions. - """ + """ return [ deserialize_attr_def(attr_def_data) for attr_def_data in attr_defs_data From 683bc0e39a303189149ea86f3db9747e9cb0a498 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:51:54 +0100 Subject: [PATCH 08/24] fix import --- client/ayon_core/lib/attribute_definitions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 9e1a92b18e..68c84276cb 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -10,7 +10,6 @@ import typing from typing import ( Any, Optional, - Tuple, List, Set, Dict, @@ -22,7 +21,7 @@ from typing import ( import clique if typing.TYPE_CHECKING: - from typing import Self, Union, Pattern + from typing import Tuple, Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import From 07bbe08c76e58f835c1892af63395979dcfbf26b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:08:08 +0100 Subject: [PATCH 09/24] remove 'Tuple' import Looks like the import is not needed even if the typehint is used for 'EnumItemsInputType'? --- client/ayon_core/lib/attribute_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 68c84276cb..e841a4b230 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -21,7 +21,7 @@ from typing import ( import clique if typing.TYPE_CHECKING: - from typing import Tuple, Self, Union, Pattern + from typing import Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import From ed9b8fe430e1d5c21a3946f548b57c3f8b1d056f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:08:04 +0100 Subject: [PATCH 10/24] moved TypedDict to typecheck imports --- client/ayon_core/lib/attribute_definitions.py | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e841a4b230..02d468f1bb 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -14,14 +14,35 @@ from typing import ( Set, Dict, Iterable, - TypedDict, TypeVar, ) import clique if typing.TYPE_CHECKING: - from typing import Self, Union, Pattern + from typing import Self, Tuple, Union, TypedDict, Pattern + + + class EnumItemDict(TypedDict): + label: str + value: Any + + + EnumItemsInputType = Union[ + Dict[Any, str], + List[Tuple[Any, str]], + List[Any], + List[EnumItemDict] + ] + + + class FileDefItemDict(TypedDict): + directory: str + filenames: List[str] + frames: Optional[List[int]] + template: Optional[str] + is_sequence: Optional[bool] + # Global variable which store attribute definitions by type # - default types are registered on import @@ -31,22 +52,6 @@ _attr_defs_by_type = {} IntFloatType = "Union[int, float]" -class EnumItemDict(TypedDict): - label: str - value: Any - - -EnumItemsInputType = "Union[Dict[Any, str], List[Tuple[Any, str]], List[Any], List[EnumItemDict]]" # noqa: E501 - - -class FileDefItemDict(TypedDict): - directory: str - filenames: List[str] - frames: Optional[List[int]] - template: Optional[str] - is_sequence: Optional[bool] - - class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. @@ -552,7 +557,7 @@ class EnumDef(AbstractAttrDef): def __init__( self, key: str, - items: EnumItemsInputType, + items: "EnumItemsInputType", default: "Union[str, List[Any]]" = None, multiselection: Optional[bool] = False, **kwargs @@ -579,7 +584,7 @@ class EnumDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.items: List[EnumItemDict] = items + self.items: List["EnumItemDict"] = items self._item_values: Set[Any] = item_values_set self.multiselection: bool = multiselection @@ -611,7 +616,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items: EnumItemsInputType) -> List[EnumItemDict]: + def prepare_enum_items(items: "EnumItemsInputType") -> List["EnumItemDict"]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -886,7 +891,7 @@ class FileDefItem: return output @classmethod - def from_dict(cls, data: FileDefItemDict) -> "Self": + def from_dict(cls, data: "FileDefItemDict") -> "Self": return cls( data["directory"], data["filenames"], @@ -928,7 +933,7 @@ class FileDefItem: return output - def to_dict(self) -> FileDefItemDict: + def to_dict(self) -> "FileDefItemDict": output = { "is_sequence": self.is_sequence, "directory": self.directory, From ad25aa7b525276da52bf80e2686a0482954a1bec Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Thu, 7 Nov 2024 12:47:24 +0100 Subject: [PATCH 11/24] Use open -R for opening explorer on MacOS --- client/ayon_core/plugins/actions/open_file_explorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index 50a3107444..e96392ec00 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -99,7 +99,7 @@ class OpenTaskPath(LauncherAction): if platform_name == "windows": args = ["start", path] elif platform_name == "darwin": - args = ["open", "-na", path] + args = ["open", "-R", path] elif platform_name == "linux": args = ["xdg-open", path] else: From 7d23e1ac3fc29a8e8bc99ef94283f62f2b9f746f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Nov 2024 16:36:47 +0100 Subject: [PATCH 12/24] Fix support for scriptsmenu running commands in Qt6 (e.g. PySide6 in Maya 2025) --- client/ayon_core/vendor/python/scriptsmenu/action.py | 9 +++++---- .../ayon_core/vendor/python/scriptsmenu/launchformaya.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/vendor/python/scriptsmenu/action.py b/client/ayon_core/vendor/python/scriptsmenu/action.py index 49b08788f9..3ba281fed7 100644 --- a/client/ayon_core/vendor/python/scriptsmenu/action.py +++ b/client/ayon_core/vendor/python/scriptsmenu/action.py @@ -1,6 +1,6 @@ import os -from qtpy import QtWidgets +from qtpy import QtWidgets, QT6 class Action(QtWidgets.QAction): @@ -112,20 +112,21 @@ module.{module_name}()""" Run the command of the instance or copy the command to the active shelf based on the current modifiers. - If callbacks have been registered with fouind modifier integer the + If callbacks have been registered with found modifier integer the function will trigger all callbacks. When a callback function returns a non zero integer it will not execute the action's command - """ # get the current application and its linked keyboard modifiers app = QtWidgets.QApplication.instance() modifiers = app.keyboardModifiers() + if not QT6: + modifiers = int(modifiers) # If the menu has a callback registered for the current modifier # we run the callback instead of the action itself. registered = self._root.registered_callbacks - callbacks = registered.get(int(modifiers), []) + callbacks = registered.get(modifiers, []) for callback in callbacks: signal = callback(self) if signal != 0: diff --git a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py index 496278ac6f..a5503bc63e 100644 --- a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py +++ b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py @@ -4,7 +4,7 @@ import maya.cmds as cmds import maya.mel as mel import scriptsmenu -from qtpy import QtCore, QtWidgets +from qtpy import QtCore, QtWidgets, QT6 log = logging.getLogger(__name__) @@ -130,7 +130,7 @@ def main(title="Scripts", parent=None, objectName=None): # Register control + shift callback to add to shelf (maya behavior) modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier - if int(cmds.about(version=True)) < 2025: + if not QT6: modifiers = int(modifiers) menu.register_callback(modifiers, to_shelf) From 2337d116d54eaabfd73b81f9f45c1865e124a65a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:46:53 +0100 Subject: [PATCH 13/24] change is_latest based on version item --- client/ayon_core/tools/sceneinventory/model.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index b7f79986ac..9b1e75a0d1 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -194,14 +194,14 @@ class InventoryModel(QtGui.QStandardItemModel): group_items = [] for repre_id, container_items in items_by_repre_id.items(): repre_info = repre_info_by_id[repre_id] - version_label = "N/A" version_color = None - is_latest = False - is_hero = False - status_name = None if not repre_info.is_valid: + version_label = "N/A" group_name = "< Entity N/A >" item_icon = invalid_item_icon + is_latest = False + is_hero = False + status_name = None else: group_name = "{}_{}: ({})".format( @@ -217,6 +217,7 @@ class InventoryModel(QtGui.QStandardItemModel): version_item = version_items[repre_info.version_id] version_label = format_version(version_item.version) is_hero = version_item.version < 0 + is_latest = version_item.is_latest if not version_item.is_latest: version_color = self.OUTDATED_COLOR status_name = version_item.status From 8a7239fc0511c19bfb1f1a0bf0e01d18b9026fa8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:47:04 +0100 Subject: [PATCH 14/24] remove unncessary line --- client/ayon_core/tools/sceneinventory/models/containers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 871455c96b..4f3ddf1ded 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -383,7 +383,6 @@ class ContainersModel: container_items_by_id[item.item_id] = item container_items.append(item) - self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id self._items_cache = container_items From 1de069c324a8d49c7e1424fe7d0fd95539238145 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:47:15 +0100 Subject: [PATCH 15/24] remove unnessary conversion --- client/ayon_core/tools/sceneinventory/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 9b1e75a0d1..bdcd183c99 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -426,7 +426,7 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): state = bool(state) if state != self._filter_outdated: - self._filter_outdated = bool(state) + self._filter_outdated = state self.invalidateFilter() def set_hierarchy_view(self, state): From 749984c0bff74c4491a7d6e853afa70906b1e984 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 8 Nov 2024 13:10:44 +0100 Subject: [PATCH 16/24] Fix loader load option box widgets --- client/ayon_core/tools/attribute_defs/widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 22f4bfe535..93f63730f5 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -202,7 +202,6 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): self._current_keys.add(attr_def.key) widget = create_widget_for_attr_def(attr_def, self) - self._widgets.append(widget) self._widgets_by_id[attr_def.id] = widget if not attr_def.visible: From 20206a3cf3444c3a74b8f3aa046985def1cbfa38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:57:00 +0100 Subject: [PATCH 17/24] check executable name before killing the process --- client/ayon_core/tools/tray/lib.py | 103 ++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 39fcc2cdd3..94550775e6 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -3,12 +3,9 @@ import sys import json import hashlib import platform -import subprocess -import csv import time import signal -import locale -from typing import Optional, Dict, Tuple, Any +from typing import Optional, List, Dict, Tuple, Any import requests from ayon_api.utils import get_default_settings_variant @@ -53,15 +50,99 @@ def _get_server_and_variant( return server_url, variant +def _windows_get_pid_args(pid: int) -> Optional[List[str]]: + import ctypes + from ctypes import wintypes + + # Define constants + PROCESS_COMMANDLINE_INFO = 60 + STATUS_NOT_FOUND = 0xC0000225 + PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + + # Define the UNICODE_STRING structure + class UNICODE_STRING(ctypes.Structure): + _fields_ = [ + ("Length", wintypes.USHORT), + ("MaximumLength", wintypes.USHORT), + ("Buffer", wintypes.LPWSTR) + ] + + shell32 = ctypes.WinDLL("shell32", use_last_error=True) + + CommandLineToArgvW = shell32.CommandLineToArgvW + CommandLineToArgvW.argtypes = [ + wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int) + ] + CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR) + + output = None + # Open the process + handle = ctypes.windll.kernel32.OpenProcess( + PROCESS_QUERY_LIMITED_INFORMATION, False, pid + ) + if not handle: + return output + + try: + buffer_len = wintypes.ULONG() + # Get the right buffer size first + status = ctypes.windll.ntdll.NtQueryInformationProcess( + handle, + PROCESS_COMMANDLINE_INFO, + ctypes.c_void_p(None), + 0, + ctypes.byref(buffer_len) + ) + + if status == STATUS_NOT_FOUND: + return output + + # Create buffer with collected size + buffer = ctypes.create_string_buffer(buffer_len.value) + + # Get the command line + status = ctypes.windll.ntdll.NtQueryInformationProcess( + handle, + PROCESS_COMMANDLINE_INFO, + buffer, + buffer_len, + ctypes.byref(buffer_len) + ) + if status: + return output + # Build the string + tmp = ctypes.cast(buffer, ctypes.POINTER(UNICODE_STRING)).contents + size = tmp.Length // 2 + 1 + cmdline_buffer = ctypes.create_unicode_buffer(size) + ctypes.cdll.msvcrt.wcscpy(cmdline_buffer, tmp.Buffer) + + args_len = ctypes.c_int() + args = CommandLineToArgvW( + cmdline_buffer, ctypes.byref(args_len) + ) + output = [args[idx] for idx in range(args_len.value)] + ctypes.windll.kernel32.LocalFree(args) + + finally: + ctypes.windll.kernel32.CloseHandle(handle) + return output def _windows_pid_is_running(pid: int) -> bool: - args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"] - output = subprocess.check_output(args) - encoding = locale.getpreferredencoding() - csv_content = csv.DictReader(output.decode(encoding).splitlines()) - # if "PID" not in csv_content.fieldnames: - # return False - for _ in csv_content: + args = _windows_get_pid_args(pid) + if not args: + return False + executable_path = args[0] + + filename = os.path.basename(executable_path).lower() + if "ayon" in filename: return True + + # Try to handle tray running from code + # - this might be potential danger that kills other python process running + # 'start.py' script (low chance, but still) + if "python" in filename and len(args) > 1: + script_filename = os.path.basename(args[1].lower()) + if script_filename == "start.py": + return True return False From 41db386f23f4ec18e870abe1817d0f71eb8fc775 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:59:05 +0100 Subject: [PATCH 18/24] add empty lines --- client/ayon_core/tools/tray/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 94550775e6..13ee1eea5c 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -126,6 +126,8 @@ def _windows_get_pid_args(pid: int) -> Optional[List[str]]: finally: ctypes.windll.kernel32.CloseHandle(handle) return output + + def _windows_pid_is_running(pid: int) -> bool: args = _windows_get_pid_args(pid) if not args: From 262cc0e7bb117516b1c1a3a7ef3b71d0508c8adf Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 8 Nov 2024 18:07:10 +0000 Subject: [PATCH 19/24] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 2b2af81e18..74f64e7944 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.6+dev" +__version__ = "1.0.7" diff --git a/package.py b/package.py index 59f0e82be0..c3fc02b625 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.6+dev" +version = "1.0.7" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index ca626eff00..12a68630e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.6+dev" +version = "1.0.7" description = "" authors = ["Ynput Team "] readme = "README.md" From 7ae9b1815378352ed86f7b0dee251d58995bf11a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 8 Nov 2024 18:07:45 +0000 Subject: [PATCH 20/24] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 74f64e7944..3a5b63785d 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.7" +__version__ = "1.0.7+dev" diff --git a/package.py b/package.py index c3fc02b625..ef2f3822eb 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.7" +version = "1.0.7+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 12a68630e2..78a3021b30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.7" +version = "1.0.7+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From a6729802dc6c0f1cd26dbf9447536d947797f906 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:58:47 +0100 Subject: [PATCH 21/24] make sure version combobox has no focus policy --- client/ayon_core/tools/loader/ui/products_delegates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 9753da37af..fba9b5b3ca 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -222,6 +222,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): editor = VersionComboBox(product_id, parent) editor.setProperty("itemId", item_id) + editor.setFocusPolicy(QtCore.Qt.NoFocus) editor.value_changed.connect(self._on_editor_change) editor.destroyed.connect(self._on_destroy) From a089b17f2ffc673830520d95976a57148905a965 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:20:21 +0100 Subject: [PATCH 22/24] added '__required_keys' to CreatedInstance --- client/ayon_core/pipeline/create/structures.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index ba4a373597..fdd41b7255 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -434,6 +434,13 @@ class CreatedInstance: "creator_attributes", "publish_attributes" ) + # Keys that can be changed, but should not be removed from instance + __required_keys = { + "folderPath": None, + "task": None, + "productName": None, + "active": True, + } def __init__( self, @@ -515,6 +522,9 @@ class CreatedInstance: if data: self._data.update(data) + for key, default in self.__required_keys.items(): + self._data.setdefault(key, default) + if not self._data.get("instance_id"): self._data["instance_id"] = str(uuid4()) @@ -567,6 +577,8 @@ class CreatedInstance: has_key = key in self._data output = self._data.pop(key, *args, **kwargs) if has_key: + if key in self.__required_keys: + self._data[key] = self.__required_keys[key] self._create_context.instance_values_changed( self.id, {key: None} ) From 2cf62f0bb455c401897f85f641774e70770ca1fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:20:35 +0100 Subject: [PATCH 23/24] fix product type key in immutable keys --- client/ayon_core/pipeline/create/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index fdd41b7255..a1a4d5f8ef 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -429,7 +429,7 @@ class CreatedInstance: __immutable_keys = ( "id", "instance_id", - "product_type", + "productType", "creator_identifier", "creator_attributes", "publish_attributes" From b79e0189a073b579f9e45a0496ec6f87cbb3a617 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:43:17 +0100 Subject: [PATCH 24/24] Use N/A label if is not available --- client/ayon_core/tools/publisher/models/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index ca26749b65..9644af43e0 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -296,7 +296,7 @@ class InstanceItem: return InstanceItem( instance.id, instance.creator_identifier, - instance.label, + instance.label or "N/A", instance.group_label, instance.product_type, instance.product_name,