From e629e40b4f95718e94de5c63180c38251c3c8e54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Mar 2022 18:58:33 +0100 Subject: [PATCH] created base of event system --- openpype/pipeline/__init__.py | 8 ++ openpype/pipeline/events.py | 221 ++++++++++++++++++++++++++++++ openpype/pipeline/lib/__init__.py | 8 -- openpype/pipeline/lib/events.py | 51 ------- 4 files changed, 229 insertions(+), 59 deletions(-) create mode 100644 openpype/pipeline/events.py delete mode 100644 openpype/pipeline/lib/events.py diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index e968df4011..673608bded 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -1,5 +1,10 @@ from .lib import attribute_definitions +from .events import ( + emit_event, + register_event_callback +) + from .create import ( BaseCreator, Creator, @@ -17,6 +22,9 @@ from .publish import ( __all__ = ( "attribute_definitions", + "emit_event", + "register_event_callback", + "BaseCreator", "Creator", "AutoCreator", diff --git a/openpype/pipeline/events.py b/openpype/pipeline/events.py new file mode 100644 index 0000000000..cae8b250f7 --- /dev/null +++ b/openpype/pipeline/events.py @@ -0,0 +1,221 @@ +"""Events holding data about specific event.""" +import os +import re +import inspect +import logging +import weakref +from uuid import uuid4 +try: + from weakref import WeakMethod +except Exception: + from .python_2_comp import WeakMethod + + +class EventCallback(object): + def __init__(self, topic, func_ref, func_name, func_path): + self._topic = topic + # Replace '*' with any character regex and escape rest of text + # - when callback is registered for '*' topic it will receive all + # events + # - it is possible to register to a partial topis 'my.event.*' + # - it will receive all matching event topics + # e.g. 'my.event.start' and 'my.event.end' + topic_regex_str = "^{}$".format( + ".+".join( + re.escape(part) + for part in topic.split("*") + ) + ) + topic_regex = re.compile(topic_regex_str) + self._topic_regex = topic_regex + self._func_ref = func_ref + self._func_name = func_name + self._func_path = func_path + self._ref_valid = True + self._enabled = True + + self._log = None + + def __repr__(self): + return "< {} - {} > {}".format( + self.__class__.__name__, self._func_name, self._func_path + ) + + @property + def log(self): + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + + @property + def is_ref_valid(self): + return self._ref_valid + + def validate_ref(self): + if not self._ref_valid: + return + + callback = self._func_ref() + if not callback: + self._ref_valid = False + + @property + def enabled(self): + """Is callback enabled.""" + return self._enabled + + def set_enabled(self, enabled): + """Change if callback is enabled.""" + self._enabled = enabled + + def deregister(self): + """Calling this funcion will cause that callback will be removed.""" + # Fake reference + self._ref_valid = False + + def topic_matches(self, topic): + """Check if event topic matches callback's topic.""" + return self._topic_regex.match(topic) + + def process_event(self, event): + """Process event. + + Args: + 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: + 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 + + elif self.topic_matches(event.topic): + # Try execute callback + sig = inspect.signature(callback) + try: + if len(sig.parameters) == 0: + callback() + else: + callback(event) + except Exception: + self.log.warning( + "Failed to execute event callback {}".format( + str(repr(self)) + ), + exc_info=True + ) + + +# Inherit from 'object' for Python 2 hosts +class Event(object): + """Base event object. + + Can be used to anything because data are not much specific. Only required + argument is topic which defines why event is happening and may be used for + filtering. + + Arg: + topic (str): Identifier of event. + data (Any): Data specific for event. Dictionary is recommended. + """ + _data = {} + + def __init__(self, topic, data=None, source=None): + self._id = str(uuid4()) + self._topic = topic + if data is None: + data = {} + self._data = data + self._source = source + + def __getitem__(self, key): + return self._data[key] + + def get(self, key, *args, **kwargs): + return self._data.get(key, *args, **kwargs) + + @property + def id(self): + return self._id + + @property + def source(self): + return self._source + + @property + def data(self): + return self._data + + @property + def topic(self): + return self._topic + + def emit(self): + """Emit event and trigger callbacks.""" + StoredCallbacks.emit_event(self) + + +class StoredCallbacks: + _registered_callbacks = [] + + @classmethod + def add_callback(cls, topic, callback): + # Convert callback into references + # - deleted functions won't cause crashes + if inspect.ismethod(callback): + ref = WeakMethod(callback) + elif callable(callback): + ref = weakref.ref(callback) + else: + # TODO add logs + return + + function_name = callback.__name__ + function_path = os.path.abspath(inspect.getfile(callback)) + callback = EventCallback(topic, ref, function_name, function_path) + cls._registered_callbacks.append(callback) + return callback + + @classmethod + def validate(cls): + invalid_callbacks = [] + for callbacks in cls._registered_callbacks: + for callback in tuple(callbacks): + callback.validate_ref() + if not callback.is_ref_valid: + invalid_callbacks.append(callback) + + for callback in invalid_callbacks: + cls._registered_callbacks.remove(callback) + + @classmethod + def emit_event(cls, event): + invalid_callbacks = [] + for callback in cls._registered_callbacks: + callback.process_event() + if not callback.is_ref_valid: + invalid_callbacks.append(callback) + + for callback in invalid_callbacks: + cls._registered_callbacks.remove(callback) + + +def register_event_callback(topic, callback): + """Add callback that will be executed on specific topic.""" + return StoredCallbacks.add_callback(topic, callback) + + +def emit_event(topic, data=None, source=None): + """Emit event with topic and data. + + Returns: + Event: Object of event that was emitted. + """ + event = Event(topic, data, source) + event.emit() + return event diff --git a/openpype/pipeline/lib/__init__.py b/openpype/pipeline/lib/__init__.py index ed38889c66..f762c4205d 100644 --- a/openpype/pipeline/lib/__init__.py +++ b/openpype/pipeline/lib/__init__.py @@ -1,8 +1,3 @@ -from .events import ( - BaseEvent, - BeforeWorkfileSave -) - from .attribute_definitions import ( AbtractAttrDef, @@ -20,9 +15,6 @@ from .attribute_definitions import ( __all__ = ( - "BaseEvent", - "BeforeWorkfileSave", - "AbtractAttrDef", "UIDef", diff --git a/openpype/pipeline/lib/events.py b/openpype/pipeline/lib/events.py deleted file mode 100644 index 05dea20e8c..0000000000 --- a/openpype/pipeline/lib/events.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Events holding data about specific event.""" - - -# Inherit from 'object' for Python 2 hosts -class BaseEvent(object): - """Base event object. - - Can be used to anything because data are not much specific. Only required - argument is topic which defines why event is happening and may be used for - filtering. - - Arg: - topic (str): Identifier of event. - data (Any): Data specific for event. Dictionary is recommended. - """ - _data = {} - - def __init__(self, topic, data=None): - self._topic = topic - if data is None: - data = {} - self._data = data - - @property - def data(self): - return self._data - - @property - def topic(self): - return self._topic - - @classmethod - def emit(cls, *args, **kwargs): - """Create object of event and emit. - - Args: - Same args as '__init__' expects which may be class specific. - """ - from avalon import pipeline - - obj = cls(*args, **kwargs) - pipeline.emit(obj.topic, [obj]) - return obj - - -class BeforeWorkfileSave(BaseEvent): - """Before workfile changes event data.""" - def __init__(self, filename, workdir): - super(BeforeWorkfileSave, self).__init__("before.workfile.save") - self.filename = filename - self.workdir_path = workdir