From 2db02d0d7a06ad634a08228ca92ab39d6e42713a Mon Sep 17 00:00:00 2001
From: Christoph Schmidt <christoph.,schmidt@tugraz.at>
Date: Wed, 6 Dec 2023 09:42:31 +0100
Subject: [PATCH] Allows now to define properties that automatically emits
 signals when emitted

---
 examples/example1/example1.py             |   2 +-
 examples/example1/mp_process.py           |   6 +-
 examples/example2/ChildProcess2.py        |   6 +-
 examples/example2/ChildProcessControl2.py |  10 +--
 examples/example2/example2.py             |   3 +-
 examples/example3/ChildProcess3.py        |  23 ++++-
 examples/example3/ChildProcessControl3.py |  12 +--
 examples/example3/example3.py             |   3 +-
 src/cmp/CProcess.py                       | 105 ++++++++++++++++------
 src/cmp/CProcessControl.py                |  73 +++++++++++----
 src/cmp/CResultRecord.py                  |   6 +-
 tests/test_interchange_commands.py        |  10 +--
 12 files changed, 186 insertions(+), 73 deletions(-)

diff --git a/examples/example1/example1.py b/examples/example1/example1.py
index 1148565..24e023c 100644
--- a/examples/example1/example1.py
+++ b/examples/example1/example1.py
@@ -22,7 +22,7 @@ class Form(QDialog):
     def __init__(self, parent=None):
         super().__init__(parent)
 
-        child_con = ChildControl(self, enable_internal_logging=True)
+        child_con = ChildControl(self, internal_logging=True)
 
         child_con.call_without_mp_finished.connect(self.updateUI)
         child_con.call_without_mp2_changed.connect(self.updateUI2)
diff --git a/examples/example1/mp_process.py b/examples/example1/mp_process.py
index 4d93f4f..fd55ca8 100644
--- a/examples/example1/mp_process.py
+++ b/examples/example1/mp_process.py
@@ -47,9 +47,9 @@ class ChildControl(cmp.CProcessControl, Sceleton):
     call_without_mp_finished = Signal(int)
     call_without_mp2_changed = Signal(int, int, int)
 
-    def __init__(self, parent, enable_internal_logging):
-        super().__init__(parent, enable_internal_logging=enable_internal_logging)
-        self.register_child_process(ChildProc(self.state_queue, self.cmd_queue, enable_interal_logging=enable_internal_logging))
+    def __init__(self, parent, internal_logging):
+        super().__init__(parent, internal_logging=internal_logging)
+        self.register_child_process(ChildProc(self.state_queue, self.cmd_queue, enable_interal_logging=internal_logging))
 
     @cmp.CProcessControl.register_function()
     def call_without_mp(self, a, b, c=None):
diff --git a/examples/example2/ChildProcess2.py b/examples/example2/ChildProcess2.py
index 837a02c..f66c3d4 100644
--- a/examples/example2/ChildProcess2.py
+++ b/examples/example2/ChildProcess2.py
@@ -6,8 +6,10 @@ import cmp
 
 class ChildProcess2(cmp.CProcess):
 
-    def __init__(self, state_queue, cmd_queue, enable_internal_logging):
-        super().__init__(state_queue, cmd_queue, enable_internal_logging=enable_internal_logging)
+    def __init__(self, state_queue, cmd_queue, kill_flag, internal_logging, internal_log_level):
+        super().__init__(state_queue, cmd_queue, kill_flag,
+                         internal_logging=internal_logging,
+                         internal_log_level=internal_log_level)
         self.logger = None
 
     def postrun_init(self):
diff --git a/examples/example2/ChildProcessControl2.py b/examples/example2/ChildProcessControl2.py
index b8ad925..6506870 100644
--- a/examples/example2/ChildProcessControl2.py
+++ b/examples/example2/ChildProcessControl2.py
@@ -8,12 +8,10 @@ class ChildProcessControl2(cmp.CProcessControl):
     call_without_mp_finished = Signal(int)
     #call_without_mp2_changed = Signal(int, int, int)
 
-    def __init__(self, parent, signal_class, enable_internal_logging):
-        super().__init__(parent, signal_class, enable_internal_logging=enable_internal_logging)
-        self.register_child_process(ChildProcess2(
-            self.state_queue,
-            self.cmd_queue,
-            enable_internal_logging=enable_internal_logging))
+    def __init__(self, parent, signal_class, internal_logging, internal_logging_level):
+        super().__init__(parent, signal_class,
+                         internal_logging=internal_logging, internal_logging_level=internal_logging_level)
+        self.register_child_process(ChildProcess2)
 
     @cmp.CProcessControl.register_function()
     def call_without_mp(self, a, b, c=None):
diff --git a/examples/example2/example2.py b/examples/example2/example2.py
index 822b1a7..1dee498 100644
--- a/examples/example2/example2.py
+++ b/examples/example2/example2.py
@@ -4,6 +4,7 @@ Author(s): Christoph Schmidt <christoph.schmidt@tugraz.at>
 Created: 2023-10-19 12:35
 Package Version:
 """
+import logging
 import signal
 import sys
 from multiprocessing import Process, Queue, Pipe
@@ -30,7 +31,7 @@ class Form(QDialog):
         super().__init__(parent)
 
         mysignals = MySignals()
-        child_con = ChildProcessControl2(self, mysignals, enable_internal_logging=True)
+        child_con = ChildProcessControl2(self, mysignals, internal_logging=True, internal_logging_level=logging.DEBUG)
 
         mysignals.call_without_mp2_changed.connect(self.updateUI)
 
diff --git a/examples/example3/ChildProcess3.py b/examples/example3/ChildProcess3.py
index fbfaf6a..9d7a37e 100644
--- a/examples/example3/ChildProcess3.py
+++ b/examples/example3/ChildProcess3.py
@@ -2,19 +2,36 @@ import os
 import time
 
 import cmp
+from cmp.CProperty import CProperty, Cache
 
 
 class ChildProcess3(cmp.CProcess):
 
-    def __init__(self, state_queue, cmd_queue, enable_internal_logging):
-        super().__init__(state_queue, cmd_queue, enable_internal_logging=enable_internal_logging)
+    def __init__(self, state_queue, cmd_queue, kill_flag, internal_logging, internal_log_level):
+        super().__init__(state_queue, cmd_queue, kill_flag,
+                         internal_logging=internal_logging,
+                         internal_log_level=internal_log_level)
         self.logger = None
 
     def postrun_init(self):
         self.logger, self.logger_h = self.create_new_logger(f"{self.__class__.__name__}-({os.getpid()})")
 
+
     @cmp.CProcess.register_for_signal()
     def test_call(self, a):
         self.logger.info(f"{os.getpid()} -> test_call!")
         time.sleep(1)
-        return a
\ No newline at end of file
+        self.test_call2 = 1
+        return a
+
+    #@CProperty
+    #def test_call2(self, value: int = 0):
+    #    self.my_value = value
+
+    @CProperty
+    def test_call2(self, value: int):
+        self.my_value = value
+
+    @test_call2.setter(emit_to='bar')
+    def test_call2(self, value: int):
+        self.my_value = value
\ No newline at end of file
diff --git a/examples/example3/ChildProcessControl3.py b/examples/example3/ChildProcessControl3.py
index b9c4e6c..936fa38 100644
--- a/examples/example3/ChildProcessControl3.py
+++ b/examples/example3/ChildProcessControl3.py
@@ -6,13 +6,13 @@ from ChildProcess3 import ChildProcess3
 
 class ChildProcessControl3(cmp.CProcessControl):
     mp_finished = Signal(int, name='mp_finished')
+    mp_finished_untriggered = Signal(int)
 
-    def __init__(self, parent, enable_internal_logging):
-        super().__init__(parent, enable_internal_logging=enable_internal_logging)
-        self.register_child_process(ChildProcess3(
-            self.state_queue,
-            self.cmd_queue,
-            enable_internal_logging=enable_internal_logging))
+    def __init__(self, parent, internal_logging, internal_logging_level):
+        super().__init__(parent,
+                         internal_logging=internal_logging,
+                         internal_logging_level=internal_logging_level)
+        self.register_child_process(ChildProcess3)
 
     @cmp.CProcessControl.register_function(signal=mp_finished)
     def test_call(self, a):
diff --git a/examples/example3/example3.py b/examples/example3/example3.py
index 235c989..8053c48 100644
--- a/examples/example3/example3.py
+++ b/examples/example3/example3.py
@@ -4,6 +4,7 @@ Author(s): Christoph Schmidt <christoph.schmidt@tugraz.at>
 Created: 2023-10-19 12:35
 Package Version:
 """
+import logging
 import signal
 import sys
 from multiprocessing import Process, Queue, Pipe
@@ -23,7 +24,7 @@ class Form(QDialog):
     def __init__(self, parent=None):
         super().__init__(parent)
 
-        child_con = ChildProcessControl3(self, enable_internal_logging=True)
+        child_con = ChildProcessControl3(self, internal_logging=True, internal_logging_level=logging.DEBUG)
 
         child_con.mp_finished.connect(self.updateUI)
 
diff --git a/src/cmp/CProcess.py b/src/cmp/CProcess.py
index f09de52..9391edf 100644
--- a/src/cmp/CProcess.py
+++ b/src/cmp/CProcess.py
@@ -10,10 +10,14 @@ import cmp
 
 class CProcess(Process):
 
-    def __init__(self, state_queue: Queue, cmd_queue: Queue, enable_internal_logging: bool = False):
+    def __init__(self, state_queue: Queue, cmd_queue: Queue, kill_flag,
+                 internal_logging: bool = False,
+                 internal_log_level=logging.DEBUG,
+                 *args, **kwargs):
         Process.__init__(self)
+        self._internal_log_enabled = internal_logging
+        self._internal_log_level = internal_log_level
 
-        self._enable_internal_logging = enable_internal_logging
         self.logger = None
         self.logger_handler = None
 
@@ -22,11 +26,11 @@ class CProcess(Process):
 
         self.cmd_queue = cmd_queue
         self.state_queue = state_queue
-        self._kill_flag = Value('i', 0)
-
-    def register_kill_flag(self, kill_flag: Value):
         self._kill_flag = kill_flag
 
+    # ==================================================================================================================
+    #   Logging
+    # ==================================================================================================================
     def create_new_logger(self, name: str) -> (logging.Logger, logging.Handler):
         _handler = logging.handlers.QueueHandler(self.state_queue)
         _logger = logging.getLogger(name)
@@ -37,15 +41,36 @@ class CProcess(Process):
         _handler.setFormatter(formatter)
         return _logger, _handler
 
-    def enable_internal_logging(self, enable: bool):
-        self._enable_internal_logging = enable
-        if self._internal_logger is not None and enable is False:
-            self._internal_logger.disabled = True
-            #self._internal_logger.handlers = []
-        elif self._internal_logger is not None and enable is True:
-            self._internal_logger.disabled = False
-            #self._internal_logger.handlers = [self._il_handler]
+    @property
+    def internal_log_enabled(self):
+        self._internal_logger.debug(f"internal_log_enabled: {not self._internal_logger.disabled}")
+        return not self._internal_logger.disabled
+
+    @internal_log_enabled.setter
+    def internal_log_enabled(self, enable: bool) -> None:
+        """
+        Enables or disables internal logging. If disabled, the internal logger will be disabled and no messages will be
+        emitted to the state queue.
+        :param enable: True to enable, False to disable
+        """
+        self._internal_logger.disabled = not enable
+
+    @property
+    def internal_log_level(self):
+        return self._internal_logger.level
+
+    @internal_log_level.setter
+    def internal_log_level(self, level: int) -> None:
+        """
+        Sets the internal logging level.
+        :param level:
+        :return:
+        """
+        self._internal_logger.setLevel(level)
 
+    # ==================================================================================================================
+    #   Process
+    # ==================================================================================================================
     def postrun_init(self):
         """
             Dummy function  for initializing e.g. loggers (some handlers are not pickable)
@@ -54,12 +79,14 @@ class CProcess(Process):
         pass
 
     def run(self):
-        self.name = f"{self.name}/{os.getpid()}"
+        self.name = f"{os.getpid()}({self.name})"
         self.postrun_init()
 
-        self._internal_logger, self._il_handler = self.create_new_logger(f"{self.__class__.__name__}-Int({os.getpid()})")
-        self.logger, self.logger_handler = self.create_new_logger(f"{self.__class__.__name__}({os.getpid()})")
-        self.enable_internal_logging(self._enable_internal_logging)
+        self._internal_logger, self._il_handler = self.create_new_logger(f"(cmp) {self.name}")
+        self.internal_log_enabled = self._internal_log_enabled
+        self.internal_log_level = self._internal_log_level
+
+        self.logger, self.logger_handler = self.create_new_logger(f"{os.getpid()}({self.__class__.__name__})")
         self._internal_logger.debug(f"Child process {self.__class__.__name__} started.")
 
         try:
@@ -69,9 +96,8 @@ class CProcess(Process):
                 except:
                     continue
                 if isinstance(cmd, cmp.CCommandRecord):
-                    self._internal_logger.info(f"Received cmd: {cmd}")
                     self._internal_logger.debug(
-                        f"cmd: {cmd}, args: {cmd.args}, kwargs: {cmd.kwargs}, Signal to emit: {cmd.signal_name}")
+                        f"Received cmd: {cmd}, args: {cmd.args}, kwargs: {cmd.kwargs}, Signal to emit: {cmd.signal_name}")
                     try:
                         cmd.execute(self)
                     except Exception as e:
@@ -89,28 +115,55 @@ class CProcess(Process):
             self._internal_logger.warning(f"Received Exception {e}! Exiting Process {os.getpid()}")
 
     def __del__(self):
-        # if self._internal_logger is not None:
-        #    self._internal_logger.warning(f"Exiting Process")
         self.cmd_queue.close()
         self.state_queue.close()
 
+    def _put_result_to_queue(self, func_name, signal_name, res):
+        self._internal_logger.debug(f"{func_name} finished. Emitting signal {signal_name} in control class.")
+        result = cmp.CResultRecord(func_name, signal_name, res)
+        self.state_queue.put(result)
+
+
     @staticmethod
-    def register_for_signal(postfix='_finished'):
+    def register_for_signal(postfix='_finished', signal_name: str = None):
         _postfix = postfix.strip() if postfix is not None else None
+        _signal_name = signal_name.strip() if signal_name is not None else None
+
+
         def register(func):
+
             def get_signature(self, *args, **kwargs):
+                func_name = f"{func.__name__}->{self.pid}"
+
+                if _signal_name is not None:
+                    kwargs['signal_name'] = _signal_name
+
                 if 'signal_name' in kwargs and kwargs['signal_name'] is not None:
                     sign = kwargs.pop('signal_name')
                 elif _postfix is not None:
                     sign = f"{func.__name__}{_postfix}"
                     self._internal_logger.debug(f"Constructing signal name for function '{func.__name__}': {sign}")
                 else:
-                    raise ValueError(f"Cannot register function '{func.__name__}' for signal. No signal name provided!")
+                    raise ValueError(f"Cannot register function '{func_name}' for signal. No signal name provided!")
                 res = func(self, *args, **kwargs)
-                self._internal_logger.debug(f"{func.__name__} finished. Emitting signal {sign} in control class.")
-                result = cmp.CResultRecord(sign, res)
-                self.state_queue.put(result)
+                self._put_result_to_queue(func_name, sign, res)
                 return res
 
             return get_signature
+
         return register
+
+    @staticmethod
+    def setter(sigal_same: str = None):
+        def register(func):
+            def get_signature(self, *args, **kwargs):
+                func_name = f"{func.__name__}->{self.pid}"
+                res = func(self, *args, **kwargs)
+                self._internal_logger.debug(f"{func_name} finished. Emitting signal {sigal_same} in control class.")
+                result = cmp.CResultRecord(func_name, sigal_same, res)
+                self.state_queue.put(result)
+                return res
+
+            return get_signature
+
+        return register
\ No newline at end of file
diff --git a/src/cmp/CProcessControl.py b/src/cmp/CProcessControl.py
index 576edc8..b5b83c1 100644
--- a/src/cmp/CProcessControl.py
+++ b/src/cmp/CProcessControl.py
@@ -5,6 +5,7 @@ import re
 import signal
 import time
 from multiprocessing import Queue, Process, Value
+from typing import Type
 
 from PySide6.QtCore import QObject, QThreadPool, Signal
 from PySide6.QtGui import QWindow
@@ -18,22 +19,24 @@ class CProcessControl(QObject):
 
     def __init__(self, parent: QObject = None,
                  signal_class: QObject = None,
-                 enable_internal_logging: bool = False):
+                 internal_logging: bool = False,
+                 internal_logging_level: int = logging.DEBUG):
         super().__init__(parent)
         # self._kill_child_process_flag = kill_child_process_flag
-        self._enable_internal_logging = enable_internal_logging
-        print(f"Parent: {type(parent)}")
+
+
         if isinstance(parent, QWidget) or isinstance(parent, QWindow):
             parent.destroyed.connect(lambda: self.safe_exit(reason="Parent destroyed."))
 
-        # Register this class as signal class (all signals will be implemented and emitted from this class)
+        # Register this class as signal class (all signals will be implemented in and emitted from this class)
         if signal_class is not None:
             self.register_signal_class(signal_class)
         else:
             self.register_signal_class(self)
 
         # The child process
-        self._child: Process = None
+        self._child: cmp.CProcess = None
+
         # Queues for data exchange
         self.cmd_queue = Queue()
         self.state_queue = Queue()
@@ -42,14 +45,23 @@ class CProcessControl(QObject):
         self.thread_manager = QThreadPool()
 
         # The child process pid
+        self._pid = os.getpid()
         self._child_process_pid = None
+
+        self.name = f"{self._pid}({self.__class__.__name__})"
+
         self._child_kill_flag = Value('i', 1)
 
-        self._internal_logger, self._il_handler = self.create_new_logger(
-            f"{self.__class__.__name__}-Int({os.getpid()})")
+        self._internal_logger, self._il_handler = self.create_new_logger(f"(cmp) {self.name}")
+        self.enable_internal_logging = internal_logging
+        self.internal_logging_level = internal_logging_level
+
         self.logger, self.logger_handler = self.create_new_logger(f"{self.__class__.__name__}({os.getpid()})")
-        self.enable_internal_logging(enable_internal_logging)
 
+
+    # ==================================================================================================================
+    # Public methods
+    # ==================================================================================================================
     def create_new_logger(self, name: str) -> (logging.Logger, logging.Handler):
         qh = RichHandler(rich_tracebacks=True)
         _internal_logger = logging.getLogger(name)
@@ -60,11 +72,34 @@ class CProcessControl(QObject):
         qh.setFormatter(formatter)
         return _internal_logger, qh
 
-    def enable_internal_logging(self, enable: bool):
-        self._enable_internal_logging = enable
-        if self._internal_logger is not None:
-            self._internal_logger.disabled = not enable
+    @property
+    def internal_log_enabled(self):
+        return not self._internal_logger.disabled
+
+    @internal_log_enabled.setter
+    def internal_log_enabled(self, enable: bool) -> None:
+        """
+        Enables or disables internal logging. If disabled, the internal logger will be disabled and no messages will be
+        emitted to the state queue.
+        :param enable: True to enable, False to disable
+        """
+        self._internal_logger.disabled = not enable
+
+    @property
+    def internal_logging_level(self):
+        return self._internal_logger.level
 
+    @internal_logging_level.setter
+    def internal_logging_level(self, level: int) -> None:
+        """
+        Sets the internal logging level.
+        :param level:
+        :return:
+        """
+        self._internal_logger.setLevel(level)
+    # ==================================================================================================================
+    #
+    # ==================================================================================================================
     def register_signal_class(self, signal_class: QObject):
         self._signal_class = signal_class
 
@@ -72,13 +107,17 @@ class CProcessControl(QObject):
     def child_process_pid(self):
         return self._child_process_pid
 
-    def register_child_process(self, child: cmp.CProcess):
-        self._internal_logger.debug(f"Registering child process {child.__class__.__name__}.")
-        self._child = child
-        self._child.register_kill_flag(self._child_kill_flag)
+    def register_child_process(self, child: Type[cmp.CProcess], *args, **kwargs):
+        self._internal_logger.debug(f"Registering child process.")
+        self._child = child(self.state_queue, self.cmd_queue,
+                            kill_flag=self._child_kill_flag,
+                            internal_logging=self.internal_log_enabled,
+                            internal_log_level=self.internal_logging_level,
+                            *args, **kwargs)
+        #self._child.register_kill_flag(self._child_kill_flag)
         self._child_process_pid = child.pid
-        self._child.enable_internal_logging(self._enable_internal_logging)
         self._child.start()
+        self._internal_logger.debug(f"Child process {self._child.name} created.")
         self.thread_manager.start(self._monitor_result_state)
 
     def _monitor_result_state(self):
diff --git a/src/cmp/CResultRecord.py b/src/cmp/CResultRecord.py
index cc3d631..54c5c84 100644
--- a/src/cmp/CResultRecord.py
+++ b/src/cmp/CResultRecord.py
@@ -5,13 +5,15 @@ from cmp import CProcessControl as CProcessControl
 
 class CResultRecord:
 
-    def __init__(self, signal_name: str, result):
+    def __init__(self, function_name: str, signal_name: str, result):
+        self.function_name: str = function_name
         self.signal_name: str = signal_name
         self.result = result
 
     def emit_signal(self, class_object: CProcessControl):
         if hasattr(class_object, '_internal_logger'):
-            class_object._internal_logger.info(f"Emitting {self} in {class_object.__class__.__name__}.")
+            class_object._internal_logger.info(f"Function {self.function_name} returned {self.result}. "
+                                               f"Emitting {self} in {class_object.__class__.__name__}.")
         emitter = getattr(class_object, self.signal_name).emit
         if isinstance(self.result, tuple):
             emitter(*self.result)
diff --git a/tests/test_interchange_commands.py b/tests/test_interchange_commands.py
index 0967f36..76186e8 100644
--- a/tests/test_interchange_commands.py
+++ b/tests/test_interchange_commands.py
@@ -10,8 +10,8 @@ import unittest
 
 class ChildProcessCustomSignals(cmp.CProcess):
 
-    def __init__(self, state_queue, cmd_queue, enable_internal_logging):
-        super().__init__(state_queue, cmd_queue, enable_internal_logging=enable_internal_logging)
+    def __init__(self, state_queue, cmd_queue, internal_logging):
+        super().__init__(state_queue, cmd_queue, internal_logging=internal_logging)
         self.logger = None
 
     def postrun_init(self):
@@ -29,10 +29,10 @@ class ChildProcessControlCustomSignals(cmp.CProcessControl):
 
     # call_without_mp2_changed = Signal(int, int, int)
 
-    def __init__(self, parent, signal_class, enable_internal_logging):
-        super().__init__(parent, signal_class, enable_internal_logging=enable_internal_logging)
+    def __init__(self, parent, signal_class, internal_logging):
+        super().__init__(parent, signal_class, internal_logging=internal_logging)
         self.register_child_process(ChildProcessCustomSignals(self.state_queue, self.cmd_queue,
-                                                              enable_internal_logging=enable_internal_logging))
+                                                              internal_logging=internal_logging))
 
     @cmp.CProcessControl.register_function()
     def call_without_mp(self, a, b, c=None):
-- 
GitLab