From aa9dbf612eda69cf03c21c086f229aaf73b600d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Jan 2024 10:51:18 +0100 Subject: [PATCH 01/13] Chore: Event callbacks can have order (#6080) * EventCallback object have order * callbacks are processed by the callback order * safer approach to get function information * modified docstring a little * added tests for ordered calbacks * fix python 2 support * added support for partial methods * removed unused '_get_func_info' * formatting fix * change test functions docstring * added test for removement of source function when partial is used * Allow order 'None' * implemented 'weakref_partial' to allow partial callbacks * minor tweaks * added support to pass additional arguments to 'wearkref_partial' * modify docstring * added 'weakref_partial' to tests * move public method before prive methods * added required order back but use '100' as default order * fix typo Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/lib/events.py | 387 +++++++++++++++---- openpype/lib/python_module_tools.py | 2 +- tests/unit/openpype/lib/test_event_system.py | 97 ++++- 3 files changed, 399 insertions(+), 87 deletions(-) diff --git a/openpype/lib/events.py b/openpype/lib/events.py index 496b765a05..774790b80a 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -16,6 +16,113 @@ class MissingEventSystem(Exception): pass +def _get_func_ref(func): + if inspect.ismethod(func): + return WeakMethod(func) + return weakref.ref(func) + + +def _get_func_info(func): + path = "" + if func is None: + return "", path + + if hasattr(func, "__name__"): + name = func.__name__ + else: + name = str(func) + + # Get path to file and fallback to '' if fails + # NOTE This was added because of 'partial' functions which is handled, + # but who knows what else can cause this to fail? + try: + path = os.path.abspath(inspect.getfile(func)) + except TypeError: + pass + + return name, path + + +class weakref_partial: + """Partial function with weak reference to the wrapped function. + + Can be used as 'functools.partial' but it will store weak reference to + function. That means that the function must be reference counted + to avoid garbage collecting the function itself. + + When the referenced functions is garbage collected then calling the + weakref partial (no matter the args/kwargs passed) will do nothing. + It will fail silently, returning `None`. The `is_valid()` method can + be used to detect whether the reference is still valid. + + Is useful for object methods. In that case the callback is + deregistered when object is destroyed. + + Warnings: + Values passed as *args and **kwargs are stored strongly in memory. + That may "keep alive" objects that should be already destroyed. + It is recommended to pass only immutable objects like 'str', + 'bool', 'int' etc. + + Args: + func (Callable): Function to wrap. + *args: Arguments passed to the wrapped function. + **kwargs: Keyword arguments passed to the wrapped function. + """ + + def __init__(self, func, *args, **kwargs): + self._func_ref = _get_func_ref(func) + self._args = args + self._kwargs = kwargs + + def __call__(self, *args, **kwargs): + func = self._func_ref() + if func is None: + return + + new_args = tuple(list(self._args) + list(args)) + new_kwargs = dict(self._kwargs) + new_kwargs.update(kwargs) + return func(*new_args, **new_kwargs) + + def get_func(self): + """Get wrapped function. + + Returns: + Union[Callable, None]: Wrapped function or None if it was + destroyed. + """ + + return self._func_ref() + + def is_valid(self): + """Check if wrapped function is still valid. + + Returns: + bool: Is wrapped function still valid. + """ + + return self._func_ref() is not None + + def validate_signature(self, *args, **kwargs): + """Validate if passed arguments are supported by wrapped function. + + Returns: + bool: Are passed arguments supported by wrapped function. + """ + + func = self._func_ref() + if func is None: + return False + + new_args = tuple(list(self._args) + list(args)) + new_kwargs = dict(self._kwargs) + new_kwargs.update(kwargs) + return is_func_signature_supported( + func, *new_args, **new_kwargs + ) + + class EventCallback(object): """Callback registered to a topic. @@ -34,20 +141,37 @@ class EventCallback(object): or none arguments. When 1 argument is expected then the processed 'Event' object is passed in. - The registered callbacks don't keep function in memory so it is not - possible to store lambda function as callback. + The callbacks are validated against their reference counter, that is + achieved using 'weakref' module. That means that the callback must + be stored in memory somewhere. e.g. lambda functions are not + supported as valid callback. + + You can use 'weakref_partial' functions. In that case is partial object + stored in the callback object and reference counter is checked for + the wrapped function. Args: - topic(str): Topic which will be listened. - func(func): Callback to a topic. + topic (str): Topic which will be listened. + func (Callable): Callback to a topic. + order (Union[int, None]): Order of callback. Lower number means higher + priority. Raises: TypeError: When passed function is not a callable object. """ - def __init__(self, topic, func): + def __init__(self, topic, func, order): + if not callable(func): + raise TypeError(( + "Registered callback is not callable. \"{}\"" + ).format(str(func))) + + self._validate_order(order) + self._log = None self._topic = topic + self._order = order + self._enabled = True # Replace '*' with any character regex and escape rest of text # - when callback is registered for '*' topic it will receive all # events @@ -63,37 +187,38 @@ class EventCallback(object): topic_regex = re.compile(topic_regex_str) self._topic_regex = topic_regex - # Convert callback into references - # - deleted functions won't cause crashes - if inspect.ismethod(func): - func_ref = WeakMethod(func) - elif callable(func): - func_ref = weakref.ref(func) + # Callback function prep + if isinstance(func, weakref_partial): + partial_func = func + (name, path) = _get_func_info(func.get_func()) + func_ref = None + expect_args = partial_func.validate_signature("fake") + expect_kwargs = partial_func.validate_signature(event="fake") + else: - raise TypeError(( - "Registered callback is not callable. \"{}\"" - ).format(str(func))) + partial_func = None + (name, path) = _get_func_info(func) + # Convert callback into references + # - deleted functions won't cause crashes + func_ref = _get_func_ref(func) - # Collect function name and path to file for logging - func_name = func.__name__ - func_path = os.path.abspath(inspect.getfile(func)) - - # Get expected arguments from function spec - # - positional arguments are always preferred - expect_args = is_func_signature_supported(func, "fake") - expect_kwargs = is_func_signature_supported(func, event="fake") + # Get expected arguments from function spec + # - positional arguments are always preferred + expect_args = is_func_signature_supported(func, "fake") + expect_kwargs = is_func_signature_supported(func, event="fake") self._func_ref = func_ref - self._func_name = func_name - self._func_path = func_path + self._partial_func = partial_func + self._ref_is_valid = True self._expect_args = expect_args self._expect_kwargs = expect_kwargs - self._ref_valid = func_ref is not None - self._enabled = True + + self._name = name + self._path = path def __repr__(self): return "< {} - {} > {}".format( - self.__class__.__name__, self._func_name, self._func_path + self.__class__.__name__, self._name, self._path ) @property @@ -104,32 +229,83 @@ class EventCallback(object): @property def is_ref_valid(self): - return self._ref_valid + """ + + Returns: + bool: Is reference to callback valid. + """ + + self._validate_ref() + return self._ref_is_valid def validate_ref(self): - if not self._ref_valid: - return + """Validate if reference to callback is valid. - callback = self._func_ref() - if not callback: - self._ref_valid = False + Deprecated: + Reference is always live checkd with 'is_ref_valid'. + """ + + # Trigger validate by getting 'is_valid' + _ = self.is_ref_valid @property def enabled(self): - """Is callback enabled.""" + """Is callback enabled. + + Returns: + bool: Is callback enabled. + """ + return self._enabled def set_enabled(self, enabled): - """Change if callback is enabled.""" + """Change if callback is enabled. + + Args: + enabled (bool): Change enabled state of the callback. + """ + self._enabled = enabled def deregister(self): """Calling this function will cause that callback will be removed.""" - # Fake reference - self._ref_valid = False + + self._ref_is_valid = False + self._partial_func = None + self._func_ref = None + + def get_order(self): + """Get callback order. + + Returns: + Union[int, None]: Callback order. + """ + + return self._order + + def set_order(self, order): + """Change callback order. + + Args: + order (Union[int, None]): Order of callback. Lower number means + higher priority. + """ + + self._validate_order(order) + self._order = order + + order = property(get_order, set_order) def topic_matches(self, topic): - """Check if event topic matches callback's topic.""" + """Check if event topic matches callback's topic. + + Args: + topic (str): Topic name. + + Returns: + bool: Topic matches callback's topic. + """ + return self._topic_regex.match(topic) def process_event(self, event): @@ -139,36 +315,69 @@ class EventCallback(object): event(Event): Event that was triggered. """ - # Skip if callback is not enabled or has invalid reference - if not self._ref_valid or not self._enabled: + # Skip if callback is not enabled + if not self._enabled: return - # Get reference - callback = self._func_ref() - # Check if reference is valid or callback's topic matches the event - if not callback: - # Change state if is invalid so the callback is removed - self._ref_valid = False + # Get reference and skip if is not available + callback = self._get_callback() + if callback is None: + return - elif self.topic_matches(event.topic): - # Try execute callback - try: - if self._expect_args: - callback(event) + if not self.topic_matches(event.topic): + return - elif self._expect_kwargs: - callback(event=event) + # Try to execute callback + try: + if self._expect_args: + callback(event) - else: - callback() + elif self._expect_kwargs: + callback(event=event) - except Exception: - self.log.warning( - "Failed to execute event callback {}".format( - str(repr(self)) - ), - exc_info=True - ) + else: + callback() + + except Exception: + self.log.warning( + "Failed to execute event callback {}".format( + str(repr(self)) + ), + exc_info=True + ) + + def _validate_order(self, order): + if isinstance(order, int): + return + + raise TypeError( + "Expected type 'int' got '{}'.".format(str(type(order))) + ) + + def _get_callback(self): + if self._partial_func is not None: + return self._partial_func + + if self._func_ref is not None: + return self._func_ref() + return None + + def _validate_ref(self): + if self._ref_is_valid is False: + return + + if self._func_ref is not None: + self._ref_is_valid = self._func_ref() is not None + + elif self._partial_func is not None: + self._ref_is_valid = self._partial_func.is_valid() + + else: + self._ref_is_valid = False + + if not self._ref_is_valid: + self._func_ref = None + self._partial_func = None # Inherit from 'object' for Python 2 hosts @@ -282,30 +491,39 @@ class Event(object): class EventSystem(object): """Encapsulate event handling into an object. - System wraps registered callbacks and triggered events into single object - so it is possible to create mutltiple independent systems that have their + System wraps registered callbacks and triggered events into single object, + so it is possible to create multiple independent systems that have their topics and callbacks. - + Callbacks are stored by order of their registration, but it is possible to + manually define order of callbacks using 'order' argument within + 'add_callback'. """ + default_order = 100 + def __init__(self): self._registered_callbacks = [] - def add_callback(self, topic, callback): + def add_callback(self, topic, callback, order=None): """Register callback in event system. Args: topic (str): Topic for EventCallback. - callback (Callable): Function or method that will be called - when topic is triggered. + callback (Union[Callable, weakref_partial]): Function or method + that will be called when topic is triggered. + order (Optional[int]): Order of callback. Lower number means + higher priority. Returns: EventCallback: Created callback object which can be used to stop listening. """ - callback = EventCallback(topic, callback) + if order is None: + order = self.default_order + + callback = EventCallback(topic, callback, order) self._registered_callbacks.append(callback) return callback @@ -341,22 +559,6 @@ class EventSystem(object): event.emit() return event - def _process_event(self, event): - """Process event topic and trigger callbacks. - - Args: - event (Event): Prepared event with topic and data. - """ - - invalid_callbacks = [] - for callback in self._registered_callbacks: - callback.process_event(event) - if not callback.is_ref_valid: - invalid_callbacks.append(callback) - - for callback in invalid_callbacks: - self._registered_callbacks.remove(callback) - def emit_event(self, event): """Emit event object. @@ -366,6 +568,21 @@ class EventSystem(object): self._process_event(event) + def _process_event(self, event): + """Process event topic and trigger callbacks. + + Args: + event (Event): Prepared event with topic and data. + """ + + callbacks = tuple(sorted( + self._registered_callbacks, key=lambda x: x.order + )) + for callback in callbacks: + callback.process_event(event) + if not callback.is_ref_valid: + self._registered_callbacks.remove(callback) + class QueuedEventSystem(EventSystem): """Events are automatically processed in queue. diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index bedf19562d..4f9eb7f667 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -269,7 +269,7 @@ def is_func_signature_supported(func, *args, **kwargs): True Args: - func (function): A function where the signature should be tested. + func (Callable): A function where the signature should be tested. *args (Any): Positional arguments for function signature. **kwargs (Any): Keyword arguments for function signature. diff --git a/tests/unit/openpype/lib/test_event_system.py b/tests/unit/openpype/lib/test_event_system.py index aa3f929065..b0a011d83e 100644 --- a/tests/unit/openpype/lib/test_event_system.py +++ b/tests/unit/openpype/lib/test_event_system.py @@ -1,4 +1,9 @@ -from openpype.lib.events import EventSystem, QueuedEventSystem +from functools import partial +from openpype.lib.events import ( + EventSystem, + QueuedEventSystem, + weakref_partial, +) def test_default_event_system(): @@ -81,3 +86,93 @@ def test_manual_event_system_queue(): assert output == expected_output, ( "Callbacks were not called in correct order") + + +def test_unordered_events(): + """ + Validate if callbacks are triggered in order of their register. + """ + + result = [] + + def function_a(): + result.append("A") + + def function_b(): + result.append("B") + + def function_c(): + result.append("C") + + # Without order + event_system = QueuedEventSystem() + event_system.add_callback("test", function_a) + event_system.add_callback("test", function_b) + event_system.add_callback("test", function_c) + event_system.emit("test", {}, "test") + + assert result == ["A", "B", "C"] + + +def test_ordered_events(): + """ + Validate if callbacks are triggered by their order and order + of their register. + """ + result = [] + + def function_a(): + result.append("A") + + def function_b(): + result.append("B") + + def function_c(): + result.append("C") + + def function_d(): + result.append("D") + + def function_e(): + result.append("E") + + def function_f(): + result.append("F") + + # Without order + event_system = QueuedEventSystem() + event_system.add_callback("test", function_a) + event_system.add_callback("test", function_b, order=-10) + event_system.add_callback("test", function_c, order=200) + event_system.add_callback("test", function_d, order=150) + event_system.add_callback("test", function_e) + event_system.add_callback("test", function_f, order=200) + event_system.emit("test", {}, "test") + + assert result == ["B", "A", "E", "D", "C", "F"] + + +def test_events_partial_callbacks(): + """ + Validate if partial callbacks are triggered. + """ + + result = [] + + def function(name): + result.append(name) + + def function_regular(): + result.append("regular") + + event_system = QueuedEventSystem() + event_system.add_callback("test", function_regular) + event_system.add_callback("test", partial(function, "foo")) + event_system.add_callback("test", weakref_partial(function, "bar")) + event_system.emit("test", {}, "test") + + # Delete function should also make partial callbacks invalid + del function + event_system.emit("test", {}, "test") + + assert result == ["regular", "bar", "regular"] From 989ec8beb2947c8abef969c9e2922bea0eec2f8f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 18:03:05 +0800 Subject: [PATCH 02/13] make sure the default shader assigned to the redshift proxy --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index b3fbfb2ed9..0ed09c6007 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -137,6 +137,14 @@ class RedshiftProxyLoader(load.LoaderPlugin): cmds.connectAttr("{}.outMesh".format(rs_mesh), "{}.inMesh".format(mesh_shape)) + # TODO: use the assigned shading group as shaders if existed + # assign default shader to redshift proxy + shader_grp = next( + (shader_group for shader_group in cmds.ls(type="shadingEngine") + if shader_group=="initialShadingGroup") + ) + cmds.sets(mesh_shape, forceElement=shader_grp) + group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, parent=True, fullPath=True) From fba52796d61c9968543a5ccaec3b4cf23459535d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 18:04:03 +0800 Subject: [PATCH 03/13] assign the textures when the initial shading group is located --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 0ed09c6007..340533ccbd 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -143,7 +143,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): (shader_group for shader_group in cmds.ls(type="shadingEngine") if shader_group=="initialShadingGroup") ) - cmds.sets(mesh_shape, forceElement=shader_grp) + if shader_grp: + cmds.sets(mesh_shape, forceElement=shader_grp) group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, From a7efe2ea759a6775c42bb0b53a3992201e0f00a3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 18:14:26 +0800 Subject: [PATCH 04/13] hound --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 340533ccbd..f67fe1c529 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -139,10 +139,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy - shader_grp = next( - (shader_group for shader_group in cmds.ls(type="shadingEngine") - if shader_group=="initialShadingGroup") - ) + shader_grp = next((shader_group for shader_group + in cmds.ls(type="shadingEngine") + if shader_group=="initialShadingGroup")) if shader_grp: cmds.sets(mesh_shape, forceElement=shader_grp) From 3af7795f19e93082824e0162f0436687f13dee51 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 18:18:13 +0800 Subject: [PATCH 05/13] hound --- .../hosts/maya/plugins/load/load_redshift_proxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index f67fe1c529..7f88f1b152 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -139,11 +139,11 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy - shader_grp = next((shader_group for shader_group - in cmds.ls(type="shadingEngine") - if shader_group=="initialShadingGroup")) - if shader_grp: - cmds.sets(mesh_shape, forceElement=shader_grp) + shader_grp = next( + (shader_group for shader_group in cmds.ls(type="shadingEngine") + if shader_group == "initialShadingGroup") + ) + cmds.sets(mesh_shape, forceElement=shader_grp) group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, From bcea8967acab10bbc406fdfb0d7db4b12680f121 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 19:18:33 +0800 Subject: [PATCH 06/13] code tweaks on shader_grp's variable --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 7f88f1b152..7b657f9184 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -140,8 +140,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy shader_grp = next( - (shader_group for shader_group in cmds.ls(type="shadingEngine") - if shader_group == "initialShadingGroup") + (shader_group for shader_group in + cmds.ls("initialShadingGroup", type="shadingEngine") + ) ) cmds.sets(mesh_shape, forceElement=shader_grp) From b40cba08016a9607d4e5f82ac534e0970079bae8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 19:20:43 +0800 Subject: [PATCH 07/13] hound --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 7b657f9184..33f39ebf38 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -140,8 +140,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy shader_grp = next( - (shader_group for shader_group in - cmds.ls("initialShadingGroup", type="shadingEngine") + ( + shader_group for shader_group in + cmds.ls("initialShadingGroup", type="shadingEngine") ) ) cmds.sets(mesh_shape, forceElement=shader_grp) From 584341dcef9a4d4185ba1d159d60fcba9d0dc755 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 19:30:43 +0800 Subject: [PATCH 08/13] code tweaks on default shader grp --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 33f39ebf38..40385f34d6 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -139,13 +139,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy - shader_grp = next( - ( - shader_group for shader_group in - cmds.ls("initialShadingGroup", type="shadingEngine") - ) - ) - cmds.sets(mesh_shape, forceElement=shader_grp) + if cmds.ls("initialShadingGroup", type="shadingEngine"): + cmds.sets(mesh_shape, forceElement="initialShadingGroup") group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, From 6043d5f7d9a053cbc82936c3978ad590b2053798 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:17:45 +0100 Subject: [PATCH 09/13] openpype addon defines runtime dependencies (#6095) --- server_addon/openpype/client/pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server_addon/openpype/client/pyproject.toml b/server_addon/openpype/client/pyproject.toml index 318ceb0185..d8de9d4d96 100644 --- a/server_addon/openpype/client/pyproject.toml +++ b/server_addon/openpype/client/pyproject.toml @@ -16,3 +16,8 @@ pynput = "^1.7.2" # Timers manager - TODO remove "Qt.py" = "^1.3.3" qtawesome = "0.7.3" speedcopy = "^2.1" + +[ayon.runtimeDependencies] +OpenTimelineIO = "0.14.1" +opencolorio = "2.2.1" +Pillow = "9.5.0" From 91a1fb1cdbff63442426b1b0e6bb6768a17aa49e Mon Sep 17 00:00:00 2001 From: kaa Date: Thu, 4 Jan 2024 15:59:26 +0100 Subject: [PATCH 10/13] General: We should keep current subset version when we switch only the representation type (#4629) * keep current subset version when switch repre * added comment and safe switch repres * more clearly variable names and revert last feat * check selected but no change * fix switch hero version --- .../tools/sceneinventory/switch_dialog.py | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index ce2272df57..150e369678 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -1230,12 +1230,12 @@ class SwitchAssetDialog(QtWidgets.QDialog): version_ids = list() - version_docs_by_parent_id = {} + version_docs_by_parent_id_and_name = collections.defaultdict(dict) for version_doc in version_docs: parent_id = version_doc["parent"] - if parent_id not in version_docs_by_parent_id: - version_ids.append(version_doc["_id"]) - version_docs_by_parent_id[parent_id] = version_doc + version_ids.append(version_doc["_id"]) + name = version_doc["name"] + version_docs_by_parent_id_and_name[parent_id][name] = version_doc hero_version_docs_by_parent_id = {} for hero_version_doc in hero_version_docs: @@ -1293,13 +1293,32 @@ class SwitchAssetDialog(QtWidgets.QDialog): repre_doc = _repres.get(container_repre_name) if not repre_doc: - version_doc = version_docs_by_parent_id[subset_id] - version_id = version_doc["_id"] - repres_by_name = repre_docs_by_parent_id_by_name[version_id] - if selected_representation: - repre_doc = repres_by_name[selected_representation] + version_docs_by_name = version_docs_by_parent_id_and_name[ + subset_id + ] + + # If asset or subset are selected for switching, we use latest + # version else we try to keep the current container version. + if ( + selected_asset not in (None, container_asset_name) + or selected_subset not in (None, container_subset_name) + ): + version_name = max(version_docs_by_name) else: - repre_doc = repres_by_name[container_repre_name] + version_name = container_version["name"] + + version_doc = version_docs_by_name[version_name] + version_id = version_doc["_id"] + repres_docs_by_name = repre_docs_by_parent_id_by_name[ + version_id + ] + + if selected_representation: + repres_name = selected_representation + else: + repres_name = container_repre_name + + repre_doc = repres_docs_by_name[repres_name] error = None try: From 8f43b87d3628f1eb0ca97f5c017c64e0407bb3bb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 6 Jan 2024 03:25:10 +0000 Subject: [PATCH 11/13] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 4d7b8f372f..dba782ded4 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.2" +__version__ = "3.18.3-nightly.1" From 9df4160595cd6678a6501bdc2dc79e40ba4b264b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 6 Jan 2024 03:25:49 +0000 Subject: [PATCH 12/13] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 132e960885..2e854061d5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.18.3-nightly.1 - 3.18.2 - 3.18.2-nightly.6 - 3.18.2-nightly.5 @@ -134,7 +135,6 @@ body: - 3.15.6-nightly.3 - 3.15.6-nightly.2 - 3.15.6-nightly.1 - - 3.15.5 validations: required: true - type: dropdown From 5f837d6f0b381f0ffc4db488c563bac10127b481 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Jan 2024 11:51:28 +0100 Subject: [PATCH 13/13] AfterEffects: exposing Deadline pools fields in Publisher UI (#6079) * OP-6421 - added render family to families filter As published instances follow product type `render` now, fields wouldn't be shown for them. * OP-6421 - updated documentation * OP-6421 - added hosts filter Limits this with higher precision. --- .../deadline/plugins/publish/collect_pools.py | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index a25b149f11..9ee079b892 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -"""Collect Deadline pools. Choose default one from Settings - -""" import pyblish.api from openpype.lib import TextDef from openpype.pipeline.publish import OpenPypePyblishPluginMixin @@ -9,11 +6,35 @@ from openpype.pipeline.publish import OpenPypePyblishPluginMixin class CollectDeadlinePools(pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin): - """Collect pools from instance if present, from Setting otherwise.""" + """Collect pools from instance or Publisher attributes, from Setting + otherwise. + + Pools are used to control which DL workers could render the job. + + Pools might be set: + - directly on the instance (set directly in DCC) + - from Publisher attributes + - from defaults from Settings. + + Publisher attributes could be shown even for instances that should be + rendered locally as visibility is driven by product type of the instance + (which will be `render` most likely). + (Might be resolved in the future and class attribute 'families' should + be cleaned up.) + + """ order = pyblish.api.CollectorOrder + 0.420 label = "Collect Deadline Pools" - families = ["rendering", + hosts = ["aftereffects", + "fusion", + "harmony" + "nuke", + "maya", + "max"] + + families = ["render", + "rendering", "render.farm", "renderFarm", "renderlayer", @@ -30,7 +51,6 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, cls.secondary_pool = settings.get("secondary_pool", None) def process(self, instance): - attr_values = self.get_attr_values_from_data(instance.data) if not instance.data.get("primaryPool"): instance.data["primaryPool"] = ( @@ -60,8 +80,12 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, return [ TextDef("primaryPool", label="Primary Pool", - default=cls.primary_pool), + default=cls.primary_pool, + tooltip="Deadline primary pool, " + "applicable for farm rendering"), TextDef("secondaryPool", label="Secondary Pool", - default=cls.secondary_pool) + default=cls.secondary_pool, + tooltip="Deadline secondary pool, " + "applicable for farm rendering") ]