From a5645cb0d700ab45475ddb32baeda3dd4639299a Mon Sep 17 00:00:00 2001 From: Christoph Schmidt <christoph.,schmidt@tugraz.at> Date: Fri, 22 Dec 2023 12:13:30 +0100 Subject: [PATCH] Updated to v0.1.0. Child Process functions can be connected without triggering a signal when finished --- examples/example1/mp_process.py | 4 +- examples/example2/ChildProcess2.py | 8 +- examples/example2/example2.py | 1 + examples/example3/ChildProcess3.py | 10 +- examples/example3/ChildProcessControl3.py | 10 +- examples/example3/example3.py | 12 ++- examples/example4/CModel.py | 23 +++++ examples/example4/ChildProcess4.py | 20 ++++ examples/example4/ChildProcessControl4.py | 25 +++++ examples/example4/ExampleModel4.py | 54 ++++++++++ examples/example4/example4.py | 75 ++++++++++++++ pyproject.toml | 2 +- src/cmp/CBase.py | 88 ++++++++++++++++ src/cmp/CCommandRecord.py | 10 +- src/cmp/CProcess.py | 119 ++++++++++------------ src/cmp/CProcessControl.py | 106 ++++++++----------- src/cmp/CProperty.py | 10 +- src/cmp/testings.py | 45 ++++++++ tests/test_interchange_commands.py | 8 +- 19 files changed, 471 insertions(+), 159 deletions(-) create mode 100644 examples/example4/CModel.py create mode 100644 examples/example4/ChildProcess4.py create mode 100644 examples/example4/ChildProcessControl4.py create mode 100644 examples/example4/ExampleModel4.py create mode 100644 examples/example4/example4.py create mode 100644 src/cmp/CBase.py create mode 100644 src/cmp/testings.py diff --git a/examples/example1/mp_process.py b/examples/example1/mp_process.py index fd55ca8..4f2c538 100644 --- a/examples/example1/mp_process.py +++ b/examples/example1/mp_process.py @@ -25,13 +25,13 @@ class ChildProc(cmp.CProcess, Sceleton): def __init__(self, state_queue, cmd_queue, enable_interal_logging): super().__init__(state_queue, cmd_queue, enable_interal_logging=enable_interal_logging) - @cmp.CProcess.register_for_signal() + @cmp.CProcess.register_signal() def call_without_mp(self, a, b, c=None, **kwargs): print(f"{os.getpid()} -> call_without_mp with {a}, {b}, {c} and {kwargs}!") time.sleep(1) return c - @cmp.CProcess.register_for_signal('_changed') + @cmp.CProcess.register_signal('_changed') def call_without_mp2(self, a, b, c=None, **kwargs): print(f"{os.getpid()} -> call_without_mp2 with {a}, {b}, {c} and {kwargs}!") time.sleep(1) diff --git a/examples/example2/ChildProcess2.py b/examples/example2/ChildProcess2.py index f66c3d4..f3c2676 100644 --- a/examples/example2/ChildProcess2.py +++ b/examples/example2/ChildProcess2.py @@ -6,21 +6,21 @@ import cmp class ChildProcess2(cmp.CProcess): - def __init__(self, state_queue, cmd_queue, kill_flag, internal_logging, internal_log_level): + def __init__(self, state_queue, cmd_queue, kill_flag, internal_log, internal_log_level): super().__init__(state_queue, cmd_queue, kill_flag, - internal_logging=internal_logging, + internal_log=internal_log, 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() + @cmp.CProcess.register_signal() def call_without_mp(self, a, b, c=None, **kwargs): self.logger.info(f"{os.getpid()} -> call_without_mp with {a}, {b}, {c} and {kwargs}!") time.sleep(1) return c - @cmp.CProcess.register_for_signal('_changed') + @cmp.CProcess.register_signal('_changed') def call_without_mp2(self, a, b, c=None, **kwargs): print(f"{os.getpid()} -> call_without_mp2 with {a}, {b}, {c} and {kwargs}!") time.sleep(1) diff --git a/examples/example2/example2.py b/examples/example2/example2.py index 1dee498..1746101 100644 --- a/examples/example2/example2.py +++ b/examples/example2/example2.py @@ -54,6 +54,7 @@ class Form(QDialog): def closeEvent(self, arg__1): self.destroyed.emit() print("Form destroyed.") + if __name__ == '__main__': try: diff --git a/examples/example3/ChildProcess3.py b/examples/example3/ChildProcess3.py index 456a71f..3cf5a2e 100644 --- a/examples/example3/ChildProcess3.py +++ b/examples/example3/ChildProcess3.py @@ -7,20 +7,18 @@ from cmp.CProperty import CProperty, Cache class ChildProcess3(cmp.CProcess): - 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) + def __init__(self, state_queue, cmd_queue, kill_flag,*args, **kwargs): + super().__init__(state_queue, cmd_queue, kill_flag, *args, **kwargs) 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() + @cmp.CProcess.register_signal() def test_call(self, a): self.logger.info(f"{os.getpid()} -> test_call!") time.sleep(1) - self.test_call2 = 1 + # self.test_call2 = 1 return a @CProperty diff --git a/examples/example3/ChildProcessControl3.py b/examples/example3/ChildProcessControl3.py index 936fa38..1431586 100644 --- a/examples/example3/ChildProcessControl3.py +++ b/examples/example3/ChildProcessControl3.py @@ -8,14 +8,12 @@ class ChildProcessControl3(cmp.CProcessControl): mp_finished = Signal(int, name='mp_finished') mp_finished_untriggered = Signal(int) - def __init__(self, parent, internal_logging, internal_logging_level): - super().__init__(parent, - internal_logging=internal_logging, - internal_logging_level=internal_logging_level) + def __init__(self, parent, *args, **kwargs): + super().__init__(parent, *args, **kwargs) self.register_child_process(ChildProcess3) - @cmp.CProcessControl.register_function(signal=mp_finished) + @cmp.CProcessControl.register_function() def test_call(self, a): - pass + print(a) #print(f"{os.getpid()} -> call_without_mp with {a}, {b}, {c}!") diff --git a/examples/example3/example3.py b/examples/example3/example3.py index 8053c48..f3829bd 100644 --- a/examples/example3/example3.py +++ b/examples/example3/example3.py @@ -24,10 +24,11 @@ class Form(QDialog): def __init__(self, parent=None): super().__init__(parent) - child_con = ChildProcessControl3(self, internal_logging=True, internal_logging_level=logging.DEBUG) + child_con = ChildProcessControl3(self, internal_log=False, internal_log_level=logging.DEBUG) child_con.mp_finished.connect(self.updateUI) + self.browser = QTextBrowser() self.lineedit = QLineEdit('Type text and press <Enter>') self.lineedit.selectAll() @@ -40,13 +41,18 @@ class Form(QDialog): # self.lineedit.returnPressed.connect(lambda: child_con.call_without_mp(1, 2, c=3)) self.lineedit.returnPressed.connect(lambda: child_con.test_call(1)) + def updateUI(self, text): - print("updateUI: ", text) + #print("updateUI: ", text) + self.browser.append("->" + str(text)) + + def updateUI2(self, text): + #print("updateUI: ", text) self.browser.append("->" + str(text)) def closeEvent(self, arg__1): self.destroyed.emit() - print("Form destroyed.") + #print("Form destroyed.") if __name__ == '__main__': diff --git a/examples/example4/CModel.py b/examples/example4/CModel.py new file mode 100644 index 0000000..f043b94 --- /dev/null +++ b/examples/example4/CModel.py @@ -0,0 +1,23 @@ +import functools + +import cmp +from cmp import CProcess, CProcessControl + + +class CModel: + + def __init__(self, cprocess_control: CProcessControl): + self.cprocess_control: CProcessControl = cprocess_control + + @staticmethod + def wrap(fun): + """ Decoraetor for wrapping a property"" + """ + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + print(f"Calling {fun.__name__}!") + cmd = cmp.CCommandRecord("self._child.name", fun.__name__, *args, **kwargs) + print(cmd) + #self.cprocess_control.cmd_queue.put(cmd) + return fun(self, *args, **kwargs) + return wrapper diff --git a/examples/example4/ChildProcess4.py b/examples/example4/ChildProcess4.py new file mode 100644 index 0000000..1f53eaf --- /dev/null +++ b/examples/example4/ChildProcess4.py @@ -0,0 +1,20 @@ +import os +import time + +import cmp +from cmp.CProperty import CProperty, Cache +from ExampleModel4 import ExampleModel4 + + +class ChildProcess4(cmp.CProcess): + + def __init__(self, state_queue, cmd_queue, kill_flag,*args, **kwargs): + super().__init__(state_queue, cmd_queue, kill_flag, *args, **kwargs) + self.logger = None + + + def postrun_init(self): + # self.model = ExampleModel4(self) + pass + + diff --git a/examples/example4/ChildProcessControl4.py b/examples/example4/ChildProcessControl4.py new file mode 100644 index 0000000..a8a0846 --- /dev/null +++ b/examples/example4/ChildProcessControl4.py @@ -0,0 +1,25 @@ +from PySide6.QtCore import Signal + +import cmp +from ChildProcess4 import ChildProcess4 +from ExampleModel4 import ExampleModel4 + + +class ChildProcessControl4(cmp.CProcessControl): + mp_finished = Signal(int, name='mp_finished') + mp_finished_untriggered = Signal(int) + test_call1_finished = Signal(int, name='test_call1_finished') + + def __init__(self, parent, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + self.register_child_process(ChildProcess4) + + + @cmp.CProcessControl.register_function(signal=mp_finished) + def set_testfield1(self, a): + pass + #c: ChildProcess3 = self._child + #.execute_function(lambda a: c.test_call1(a), signal=ChildProcessControl4.test_call1_finished) + #print(a) + #print(f"{os.getpid()} -> call_without_mp with {a}, {b}, {c}!") + diff --git a/examples/example4/ExampleModel4.py b/examples/example4/ExampleModel4.py new file mode 100644 index 0000000..7f528a9 --- /dev/null +++ b/examples/example4/ExampleModel4.py @@ -0,0 +1,54 @@ +from PySide6.QtCore import Signal, QObject + +from cmp.CProperty import CProperty +from CModel import CModel + + +class ExampleModel4Signals(QObject): + testfield1_changed = Signal(int) + testfield2_changed = Signal(int) + testfield3_changed = Signal(int) + + +class ExampleModel4(CModel): + + def __init__(self, c_process_control): + super().__init__(c_process_control) + self.signals = ExampleModel4Signals(c_process_control) + + self._testfield1 = 0 + self._testfield2 = 0 + self._testfield3 = 0 + + + @property + + def testfield1(self): + return self._testfield1 + + @testfield1.setter + @CModel.wrap + def testfield1(self, value): + self._testfield1 = value + print(f"testfield1: {value}") + self.signals.testfield1_changed.emit(value) + + @property + def testfield2(self): + return self._testfield2 + + @testfield2.setter + def testfield2(self, value): + self._testfield2 = value + self.signals.testfield2_changed.emit(value) + + @property + def testfield3(self): + return self._testfield3 + + @testfield3.setter + def testfield3(self, value): + self._testfield3 = value + self.signals.testfield3_changed.emit(value) + + diff --git a/examples/example4/example4.py b/examples/example4/example4.py new file mode 100644 index 0000000..ec5b2ce --- /dev/null +++ b/examples/example4/example4.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +""" +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 +from threading import Thread + +from PySide6.QtCore import QObject, Signal, SIGNAL +from PySide6.QtWidgets import QDialog, QApplication, QTextBrowser, QLineEdit, QVBoxLayout, QMainWindow, QMessageBox + + + +sys.path.append('./src') +from ChildProcessControl4 import ChildProcessControl4 +from ExampleModel4 import ExampleModel4 + +class Form(QDialog): + on_text_converted = Signal(str) + + def __init__(self, parent=None): + super().__init__(parent) + + + + child_con = ChildProcessControl4(self, internal_log=True, internal_log_level=logging.DEBUG) + self.model = ExampleModel4(child_con) + + #self.model.signals.testfield1_changed.connect(child_con.child.test_call) + + self.model.testfield1 = 123 + + child_con.mp_finished.connect(self.updateUI) + + self.browser = QTextBrowser() + self.lineedit = QLineEdit('Type text and press <Enter>') + self.lineedit.selectAll() + layout = QVBoxLayout() + layout.addWidget(self.browser) + layout.addWidget(self.lineedit) + self.setLayout(layout) + self.lineedit.setFocus() + self.setWindowTitle('Upper') + # self.lineedit.returnPressed.connect(lambda: child_con.call_without_mp(1, 2, c=3)) + self.lineedit.returnPressed.connect(lambda: child_con.set_testfield1(1)) + + def updateUI(self, text): + print("updateUI: ", text) + self.browser.append("->" + str(text)) + + def closeEvent(self, arg__1): + self.destroyed.emit() + #print("Form destroyed.") + + +if __name__ == '__main__': + + try: + app = QApplication(sys.argv) + form = Form() + form.show() + app.exec() + except KeyboardInterrupt: + print("KeyboardInterrupt") + sys.exit(0) + + +#if __name__ == '__main__': + + #model = ExampleModel4() + #model.testfield1 = 123 diff --git a/pyproject.toml b/pyproject.toml index 10db365..1db8b99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cmp" -version = "0.0.2" +version = "0.1.0" authors = [ { name="Christoph Schmidt", email="cschmidt.fs@gmail.com" }, ] diff --git a/src/cmp/CBase.py b/src/cmp/CBase.py new file mode 100644 index 0000000..53e400f --- /dev/null +++ b/src/cmp/CBase.py @@ -0,0 +1,88 @@ +import logging + +from rich.logging import RichHandler + + +class CBase: + + # ================================================================================================================== + # Public methods + # ================================================================================================================== + def __init__(self): + self._internal_logger = None + self._internal_log_handler = None + self.name = self.__class__.__name__ + + def create_new_logger(self, logger_name: str, + logger_handler: logging.Handler = None, + logger_format: str = " %(name)s %(message)s") -> (logging.Logger, logging.Handler): + """ + Creates a new logger with the given name and handler. If no handler is given, a RichHandler will be used. + :param logger_name: + :param logger_handler: + :param logger_format: + :return: + """ + if logger_handler is None: + logger_handler = RichHandler(rich_tracebacks=True) + + _internal_logger = logging.getLogger(logger_name) + _internal_logger.handlers = [logger_handler] + _internal_logger.setLevel(logging.DEBUG) + formatter = logging.Formatter(logger_format) + logger_handler.setFormatter(formatter) + return _internal_logger, logger_handler + + @property + def internal_log_enabled(self): + if self._internal_logger is not None: + return not self._internal_logger.disabled + else: + raise Exception("Internal logger not initialized") + + @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 + """ + if self._internal_logger is not None: + if enable: + self._internal_logger.info(f"Internal logger of {self.__class__.__name__} has been enabled.") + else: + self._internal_logger.warning(f"Internal logger of {self.__class__.__name__} has been disabled.") + self._internal_logger.level = logging.ERROR + else: + raise Exception("Can't enable internal logger. Internal logger not initialized") + + @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: + """ + if self._internal_logger is not None: + if level == logging.DEBUG: + self._internal_logger.info(f"Internal log level of {self.__class__.__name__} has been set to DEBUG.") + elif level == logging.INFO: + self._internal_logger.info(f"Internal log level of {self.__class__.__name__} has been set to INFO.") + elif level == logging.WARNING: + self._internal_logger.info(f"Internal log level of {self.__class__.__name__} has been set to WARNING.") + elif level == logging.ERROR: + self._internal_logger.info(f"Internal log level of {self.__class__.__name__} has been set to ERROR.") + elif level == logging.CRITICAL: + self._internal_logger.info(f"Internal log level of {self.__class__.__name__} has been set to CRITICAL.") + else: + self._internal_logger.info(f"Internal log level of {self.__class__.__name__} has been set to {level}.") + + + self._internal_logger.setLevel(level) + else: + raise Exception("Can't set internal log level. Internal logger not initialized") + diff --git a/src/cmp/CCommandRecord.py b/src/cmp/CCommandRecord.py index 503fbbb..52472d8 100644 --- a/src/cmp/CCommandRecord.py +++ b/src/cmp/CCommandRecord.py @@ -2,10 +2,11 @@ from cmp import CProcess as CProcess class CCommandRecord: - def __init__(self, func_name: str, *args: (), **kwargs: {}): + def __init__(self, proc_name: str, func_name: str, *args: (), **kwargs: {}, ): self.func_name: str = func_name self.args: () = args self.kwargs: {} = kwargs + self.proc_name = proc_name self.signal_name: str = None @@ -15,10 +16,13 @@ class CCommandRecord: def execute(self, class_object: CProcess): if hasattr(class_object, '_internal_logger'): class_object._internal_logger.info(f"Executing {self} in {class_object.name}.") - getattr(class_object, self.func_name)(signal_name=self.signal_name, *self.args, **self.kwargs) + if self.signal_name is not None: + getattr(class_object, self.func_name)(signal_name=self.signal_name, *self.args, **self.kwargs) + else: + getattr(class_object, self.func_name)(*self.args, **self.kwargs) def __repr__(self): args_str = ', '.join(map(repr, self.args)) kwargs_str = ', '.join(f"{key}={repr(value)}" for key, value in self.kwargs.items()) all_args = ', '.join(filter(None, [args_str, kwargs_str])) - return f"Function {self.func_name}({all_args})" + return f"Function <{self.proc_name}>.{self.func_name}({all_args})" diff --git a/src/cmp/CProcess.py b/src/cmp/CProcess.py index 9391edf..a219d8d 100644 --- a/src/cmp/CProcess.py +++ b/src/cmp/CProcess.py @@ -1,73 +1,39 @@ import logging.handlers import logging +import multiprocessing import os +import sys import time import traceback from multiprocessing import Process, Queue, Value import cmp +from cmp.CBase import CBase +# This is a Queue that behaves like stdout -class CProcess(Process): - def __init__(self, state_queue: Queue, cmd_queue: Queue, kill_flag, - internal_logging: bool = False, +class CProcess(CBase, Process): + + # override the print function + + def __init__(self, state_queue: Queue, cmd_queue: Queue, + kill_flag, + internal_log: 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._internal_log_enabled_ = internal_log + self._internal_log_level_ = internal_log_level self.logger = None self.logger_handler = None - self._internal_logger = None - self._il_handler = None - self.cmd_queue = cmd_queue self.state_queue = state_queue 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) - _logger.handlers.clear() - _logger.handlers = [_handler] - _logger.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(message)s') - _handler.setFormatter(formatter) - return _logger, _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 # ================================================================================================================== @@ -78,23 +44,44 @@ class CProcess(Process): """ pass + def _typecheck(self): + if not isinstance(self.cmd_queue, multiprocessing.queues.Queue): + raise TypeError(f"cmd_queue must be of type {Queue}, not {type(self.cmd_queue)}") + + if not isinstance(self.state_queue, multiprocessing.queues.Queue): + raise TypeError(f"state_queue must be of type {Queue}, not {type(self.state_queue)}") + + return True + def run(self): self.name = f"{os.getpid()}({self.name})" - self.postrun_init() - 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._internal_logger, self._internal_log_handler = self.create_new_logger( + f"(cmp) {self.name}", + logger_handler=logging.handlers.QueueHandler(self.state_queue)) + + self.logger, self.logger_handler = self.create_new_logger(f"{os.getpid()}({self.__class__.__name__})", + logger_handler=logging.handlers.QueueHandler( + self.state_queue)) + + 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.") + sys.stderr.write = self.logger.error + sys.stdout.write = self.logger.info + + self.postrun_init() + try: + self._typecheck() while self._kill_flag.value: try: cmd = self.cmd_queue.get(block=True, timeout=1) except: continue + if isinstance(cmd, cmp.CCommandRecord): self._internal_logger.debug( f"Received cmd: {cmd}, args: {cmd.args}, kwargs: {cmd.kwargs}, Signal to emit: {cmd.signal_name}") @@ -103,10 +90,12 @@ class CProcess(Process): except Exception as e: traceback_str = ''.join(traceback.format_tb(e.__traceback__)) self._internal_logger.error(f"Exception '{e}' occurred in {cmd}!. Traceback:\n{traceback_str}") + self._internal_logger.debug(f"Command {cmd} finished.") + else: + self._internal_logger.error(f"Received unknown command {cmd}!") self._internal_logger.error(f"Control Process exited. Terminating Process {os.getpid()}") if self._kill_flag.value == 0: self._internal_logger.error(f"Process {os.getpid()} received kill signal!") - except KeyboardInterrupt: self._internal_logger.warning(f"Received KeyboardInterrupt! Exiting Process {os.getpid()}") time.sleep(1) @@ -114,22 +103,26 @@ class CProcess(Process): except Exception as e: self._internal_logger.warning(f"Received Exception {e}! Exiting Process {os.getpid()}") + self._internal_logger.debug(f"Child process monitor {self.__class__.__name__} ended.") + def __del__(self): + print(f"Child process {self.name} deleted.") 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.") + if signal_name is not None: + self._internal_logger.debug(f"{func_name} finished. Emitting signal {signal_name} in control class.") + else: + self._internal_logger.debug(f"{func_name} finished. No signal to emit.") result = cmp.CResultRecord(func_name, signal_name, res) self.state_queue.put(result) - @staticmethod - def register_for_signal(postfix='_finished', signal_name: str = None): + def register_signal(postfix=None, 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): @@ -144,7 +137,7 @@ class CProcess(Process): 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!") + sign = None res = func(self, *args, **kwargs) self._put_result_to_queue(func_name, sign, res) return res @@ -154,16 +147,16 @@ class CProcess(Process): return register @staticmethod - def setter(sigal_same: str = None): + def setter(signal_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._internal_logger.debug(f"{func_name} finished. Emitting signal {signal_same} in control class.") + result = cmp.CResultRecord(func_name, signal_same, res) self.state_queue.put(result) return res return get_signature - return register \ No newline at end of file + return register diff --git a/src/cmp/CProcessControl.py b/src/cmp/CProcessControl.py index b5b83c1..541714b 100644 --- a/src/cmp/CProcessControl.py +++ b/src/cmp/CProcessControl.py @@ -1,3 +1,4 @@ +import inspect import logging import logging.handlers import os @@ -13,18 +14,19 @@ from PySide6.QtWidgets import QWidget from rich.logging import RichHandler import cmp +from cmp.CBase import CBase -class CProcessControl(QObject): +class CProcessControl(CBase, QObject): def __init__(self, parent: QObject = None, signal_class: QObject = None, - internal_logging: bool = False, - internal_logging_level: int = logging.DEBUG): - super().__init__(parent) + internal_log: bool = False, + internal_log_level: int = logging.DEBUG): + QObject.__init__(self, parent) + CBase.__init__(self) # self._kill_child_process_flag = kill_child_process_flag - if isinstance(parent, QWidget) or isinstance(parent, QWindow): parent.destroyed.connect(lambda: self.safe_exit(reason="Parent destroyed.")) @@ -41,6 +43,7 @@ class CProcessControl(QObject): self.cmd_queue = Queue() self.state_queue = Queue() + # Thread manager for monitoring the state queue self.thread_manager = QThreadPool() @@ -52,51 +55,11 @@ class CProcessControl(QObject): self._child_kill_flag = Value('i', 1) - 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._internal_logger, self._internal_log_handler = self.create_new_logger(f"(cmp) {self.name}") + self.internal_log_enabled = internal_log + self.internal_log_level = internal_log_level self.logger, self.logger_handler = self.create_new_logger(f"{self.__class__.__name__}({os.getpid()})") - - - # ================================================================================================================== - # Public methods - # ================================================================================================================== - def create_new_logger(self, name: str) -> (logging.Logger, logging.Handler): - qh = RichHandler(rich_tracebacks=True) - _internal_logger = logging.getLogger(name) - _internal_logger.handlers = [qh] - _internal_logger.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(name)s %(message)s') - qh.setFormatter(formatter) - return _internal_logger, qh - - @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) # ================================================================================================================== # # ================================================================================================================== @@ -107,19 +70,24 @@ class CProcessControl(QObject): def child_process_pid(self): return self._child_process_pid - def register_child_process(self, child: Type[cmp.CProcess], *args, **kwargs): + def register_child_process(self, child, *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, + internal_log=self.internal_log_enabled, + internal_log_level=self.internal_log_level, *args, **kwargs) - #self._child.register_kill_flag(self._child_kill_flag) + # self._child.register_kill_flag(self._child_kill_flag) self._child_process_pid = child.pid self._child.start() self._internal_logger.debug(f"Child process {self._child.name} created.") self.thread_manager.start(self._monitor_result_state) + @property + def child(self): + return self._child + def _monitor_result_state(self): self._internal_logger.info("Starting monitor thread.") try: @@ -136,18 +104,25 @@ class CProcessControl(QObject): self.logger.warning(f"Error cannot handle log record: {e}") elif isinstance(res, cmp.CResultRecord): try: - res.emit_signal(self._signal_class) - self._internal_logger.debug(f"Emitted {res} in {self._signal_class.__class__.__name__}.") + if isinstance(res, Signal): + res.emit_signal(self._signal_class) + self._internal_logger.debug(f"Emitted {res} in {self._signal_class.__class__.__name__}.") except Exception as e: self._internal_logger.error(f"Error while emitting {res} in {self.__class__.__name__}: {e}") + else: + self._internal_logger.error(f"Received unknown result {res}!") + except: - print(f"Error in monitor thread") + self._internal_logger.error(f"Error in monitor thread") time.sleep(1) - self._internal_logger.info("Ended monitor thread.") + self._internal_logger.info(f"Ended monitor thread. Child process alive: {self._child.is_alive()}") self.state_queue.close() self.cmd_queue.close() + def execute_function(self, func: callable, signal: Signal = None): + self.register_function(signal)(func)(self) + @staticmethod def register_function(signal: Signal = None): """ @@ -157,6 +132,7 @@ class CProcessControl(QObject): :param signal: :return: """ + def register(func): def match_signal_name(_signal: Signal): pattern = re.compile(r'(\w+)\(([^)]*)\)') @@ -174,18 +150,26 @@ class CProcessControl(QObject): args = arguments.pop("args") kwargs = arguments.pop("kwargs") - cmd = cmp.CCommandRecord(name, *args, **kwargs) + cmd = cmp.CCommandRecord(self._child.name, name, *args, **kwargs) if signal is not None: - sig_name, args = match_signal_name(signal) + sig_name, sig_args = match_signal_name(signal) cmd.register_signal(sig_name) + self._internal_logger.debug(f"New function registered: {cmd} -> " + f"{cmd.signal_name if cmd.signal_name is not None else 'None'}(" + f"{', '.join(str(a) for a in sig_args) if sig_args is not None else 'None'})") else: + self._internal_logger.debug(f"New function registered: {cmd}") + + try: + self._internal_logger.debug(f"Executing {name} with args {args} and kwargs {kwargs}") func(self, *args, **kwargs) + except Exception as e: + self._internal_logger.error(f"Error while executing {cmd}: {e}") + raise e try: - self._internal_logger.debug(f"New function registered: {cmd} -> " - f"{cmd.signal_name if cmd.signal_name is not None else 'None'}(" - f"{', '.join(a for a in args) if args is not None else 'None'})") self.cmd_queue.put(cmd) + self._internal_logger.debug(f"{cmd} put into cmd_queue.") except Exception as e: self._internal_logger.error(f"Error while putting {cmd} into cmd_queue: {e}") raise e diff --git a/src/cmp/CProperty.py b/src/cmp/CProperty.py index 1cac741..9f7c597 100644 --- a/src/cmp/CProperty.py +++ b/src/cmp/CProperty.py @@ -19,12 +19,11 @@ class _Cache(object): def __set__(self, obj, value): if self.fset is None: raise AttributeError("can't set attribute") - print(f"Setting {obj}, {value}!") self.fset(obj, value) - @staticmethod - def set(signal_name): - print("************ 1 Setting!") + #@staticmethod + #def set(signal_name): + # print("************ 1 Setting!") def __call__(self, *args, **kwargs): """Invoked on every call of decorated method""" @@ -66,10 +65,8 @@ class CProperty: return self.fget(obj) def __set__(self, obj: cmp.CProcess, value): - if self.fset is None: raise AttributeError("can't set attribute") - obj._internal_logger.debug(f"Setting {self.signal_name}!") result = cmp.CResultRecord(str(self.fset.__name__), self.signal_name, value) obj.state_queue.put(result) @@ -77,6 +74,7 @@ class CProperty: self.fset(obj, value) def getter(self, fget): + return type(self)(fget, self.fset, self.signal_name) def _setter(self, fset): diff --git a/src/cmp/testings.py b/src/cmp/testings.py new file mode 100644 index 0000000..5c527ed --- /dev/null +++ b/src/cmp/testings.py @@ -0,0 +1,45 @@ +class _Cache(object): + + def __init__(self, func, fset, signal_name): + # Plain function as argument to be decorated + self.func = func + self.fset = fset + self.signal_name = signal_name + + def __get__(self, instance, owner): + self.instance_ = instance + return self.__call__ + + def __set__(self, obj, value): + if self.fset is None: + raise AttributeError("can't set attribute") + print(f"Setting {obj}, {value}!") + self.fset(obj, value) + + @staticmethod + def set(signal_name): + print("************ 1 Setting!") + + def __call__(self, *args, **kwargs): + """Invoked on every call of decorated method""" + + # set attribute on instance + name = '%s_called' % self.func.__name__ + # print(f"Setting {name} in {}!") + self.instance_._internal_logger.debug( + f"Setting {name} in {self.instance_.__class__.__name__} and emitting {self.signal_name}!") + # setattr(self.instance_, name, datetime.utcnow()) + + # returning original function with class' instance as self + return self.func(self.instance_, *args, **kwargs) + + +# wrap _Cache to allow for deferred calling +def Cache(function=None, signal_name=None): + if function: + return _Cache(function) + else: + def wrapper(function): + return _Cache(function, signal_name) + + return wrapper diff --git a/tests/test_interchange_commands.py b/tests/test_interchange_commands.py index 76186e8..30222ac 100644 --- a/tests/test_interchange_commands.py +++ b/tests/test_interchange_commands.py @@ -10,14 +10,14 @@ import unittest class ChildProcessCustomSignals(cmp.CProcess): - def __init__(self, state_queue, cmd_queue, internal_logging): - super().__init__(state_queue, cmd_queue, internal_logging=internal_logging) + def __init__(self, state_queue, cmd_queue, internal_log): + super().__init__(state_queue, cmd_queue, internal_log=internal_log) 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() + @cmp.CProcess.register_signal() def call_without_mp(self, a, b, c=None, **kwargs): self.logger.info(f"{os.getpid()} -> call_without_mp with {a}, {b}, {c} and {kwargs}!") time.sleep(1) @@ -32,7 +32,7 @@ class ChildProcessControlCustomSignals(cmp.CProcessControl): 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, - internal_logging=internal_logging)) + internal_log=internal_logging)) @cmp.CProcessControl.register_function() def call_without_mp(self, a, b, c=None): -- GitLab