From 44be04e0297a22333d2b46cdd778518fb53971bc Mon Sep 17 00:00:00 2001 From: Andrea Settimi Date: Sun, 28 Jan 2024 22:20:29 +0100 Subject: [PATCH] CAP-WIP: modifiedd functioning CI componentizer --- .../ghpython-components/componentize.py | 12 -- GH/PyGH/components/scriptsynccpy/code.py | 175 +++++++++++------- .../components/scriptsynccpy/metadata.json | 6 +- GH/PyGH/scriptsyncGH_test.py | 13 +- GH/PyGH/test/runner_script.py | 2 +- 5 files changed, 115 insertions(+), 93 deletions(-) diff --git a/.github/actions/ghpython-components/componentize.py b/.github/actions/ghpython-components/componentize.py index 57c3620..834851c 100644 --- a/.github/actions/ghpython-components/componentize.py +++ b/.github/actions/ghpython-components/componentize.py @@ -269,9 +269,6 @@ def create_ghuser_component(source, target, version=None, prefix=None): ) params.SetInt32("OutputCount", len(outputParam)) for i, _po in enumerate(outputParam): - if i == 0: - params.SetGuid("OutputId", i, System.Guid.Parse("3ede854e-c753-40eb-84cb-b48008f14fd4")) # out parameters on FIXME: whatch xml to see how it works - continue params.SetGuid( "OutputId", i, System.Guid.Parse("08908df5-fa14-4982-9ab2-1aa0927566aa") ) @@ -304,15 +301,6 @@ def create_ghuser_component(source, target, version=None, prefix=None): pi_chunk.SetInt32("Mapping", 2) for i, po in enumerate(outputParam): - # if i == 0: - # output_instance_guid = System.Guid.NewGuid() - # po_chunk = params.CreateChunk("OutputParam", i) - # po_chunk.SetString("Name", po["name"]) - # po_chunk.SetString("NickName", po.get("nickname") or po["name"]) - # po_chunk.SetString("Description", po.get("description")) - # po_chunk.SetInt32("SourceCount", po.get("sourceCount", 0)) - # po_chunk.SetGuid("InstanceGuid", output_instance_guid) - # continue output_instance_guid = System.Guid.NewGuid() po_chunk = params.CreateChunk("OutputParam", i) po_chunk.SetString("Name", po["name"]) diff --git a/GH/PyGH/components/scriptsynccpy/code.py b/GH/PyGH/components/scriptsynccpy/code.py index 401f466..1727a17 100644 --- a/GH/PyGH/components/scriptsynccpy/code.py +++ b/GH/PyGH/components/scriptsynccpy/code.py @@ -7,99 +7,136 @@ import os import time -from System.Threading import Thread -from functools import partial +import contextlib +import io + +import threading import rhinoscriptsyntax as rs -def update_component(): - """ Fire the recalculation of the component solution. """ - # clear the output - ghenv.Component.Params.Output[0].ClearData() - # expire the component - - ghenv.Component.ExpireSolution(True) - -def check_file_change(path): - """ - Check if the file has changed on disk. - - :param path: The path of the file to check. - :returns: True if the file has changed, False otherwise. - """ - last_modified = os.path.getmtime(path) - while True: - System.Threading.Thread.Sleep(1000) - current_modified = os.path.getmtime(path) - if current_modified != last_modified: - last_modified = current_modified - update_component() - break - return - -def safe_exec(path, globals, locals): - """ - Execute Python3 code safely. - - :param path: The path of the file to execute. - :param globals: The globals dictionary. - :param locals: The locals dictionary. - """ - try: - with open(path, 'r') as f: - code = compile(f.read(), path, 'exec') - exec(code, globals, locals) - return locals # return the locals dictionary - except Exception as e: - err_msg = str(e) - return e + +class ScriptSyncThread(threading.Thread): + def __init__(self, + path : str, + path_lock : threading.Lock, + name : str): + super().__init__(name=name, daemon=False) + self.path = path + self.path_lock = path_lock + self.component_on_canvas = True + + def run(self): + """ Run the thread. """ + self.check_file_change(self.path, self.path_lock) + + def check_if_component_on_canvas(self): + """ Check if the component is on canvas. """ + if ghenv.Component.OnPingDocument() is None: + self.component_on_canvas = False + + def update_component(self): + """ Fire the recalculation of the component solution. """ + ghenv.Component.Params.Output[0].ClearData() # clear the output + ghenv.Component.ExpireSolution(True) # expire the component + + def check_file_change(self, path : str, path_lock : threading.Lock) -> None: + """ + Check if the file has changed on disk. + + :param path: The path of the file to check. + :param path_lock: The lock for the path. + """ + with path_lock: + last_modified = os.path.getmtime(path) + while self.component_on_canvas: + System.Threading.Thread.Sleep(1000) + Rhino.RhinoApp.InvokeOnUiThread(System.Action(self.check_if_component_on_canvas)) + + if not self.component_on_canvas: + print(f"script-sync::Thread {self.name} aborted") + break + + current_modified = os.path.getmtime(path) + if current_modified != last_modified: + last_modified = current_modified + Rhino.RhinoApp.InvokeOnUiThread(System.Action(self.update_component)) class ScriptSyncCPy(component): def __init__(self): super(ScriptSyncCPy, self).__init__() - self._var_output = ["None"] + self._var_output = [] ghenv.Component.Message = "ScriptSyncCPy" + self.thread_name = None + self.path = None + self.path_lock = threading.Lock() + + def safe_exec(self, path, globals, locals): + """ + Execute Python3 code safely. It redirects the output of the code + to a string buffer 'stdout' to output to the GH component param. + + :param path: The path of the file to execute. + :param globals: The globals dictionary. + :param locals: The locals dictionary. + """ + try: + with open(path, 'r') as f: + code = compile(f.read(), path, 'exec') + output = io.StringIO() + with contextlib.redirect_stdout(output): + exec(code, globals, locals) + locals["stdout"] = output.getvalue() + sys.stdout = sys.__stdout__ + return locals + except Exception as e: + err_msg = f"script-sync::Error in the code: {str(e)}" + # TODO: here we need to send back the erro mesage to vscode + sys.stdout = sys.__stdout__ + raise Exception(err_msg) + def RunScript(self, x, y): """ This method is called whenever the component has to be recalculated. """ # check the file is path - path = r"F:\script-sync\GH\PyGH\test\runner_script.py" # <<<< test - if not os.path.exists(path): + self.path = r"F:\script-sync\GH\PyGH\test\runner_script.py" # <<<< test + + if not os.path.exists(self.path): raise Exception("script-sync::File does not exist") - print(f"script-sync::x value: {x}") - - # non-blocking thread - thread = Thread(partial(check_file_change, path)) - thread.Start() - + # get the guid instance of the component + self.thread_name : str = f"script-sync-thread::{ghenv.Component.InstanceGuid}" + if self.thread_name not in [t.name for t in threading.enumerate()]: + ScriptSyncThread(self.path, self.path_lock, self.thread_name).start() + # we need to add the path of the modules - path_dir = path.split("\\") + path_dir = self.path.split("\\") path_dir = "\\".join(path_dir[:-1]) sys.path.insert(0, path_dir) # run the script - res = safe_exec(path, globals(), locals()) - if isinstance(res, Exception): - err_msg = f"script-sync::Error in the code: {res}" - print(err_msg) - raise Exception(err_msg) + res = self.safe_exec(self.path, globals(), locals()) # get the output variables defined in the script outparam = ghenv.Component.Params.Output - outparam_names = [p.NickName for p in outparam if p.NickName != "out"] - for k, v in res.items(): - if k in outparam_names: - self._var_output.append(v) + outparam_names = [p.NickName for p in outparam] + for outp in outparam_names: + if outp in res.keys(): + self._var_output.append(res[outp]) + else: + self._var_output.append(None) - return self._var_output - # FIXME: problem with indexing return def AfterRunScript(self): - outparam = ghenv.Component.Params.Output + """ + This method is called as soon as the component has finished + its calculation. It is used to load the GHComponent outputs + with the values created in the script. + """ + outparam = [p for p in ghenv.Component.Params.Output] + outparam_names = [p.NickName for p in outparam] + for idx, outp in enumerate(outparam): - if outp.NickName != "out": - ghenv.Component.Params.Output[idx].VolatileData.Clear() - ghenv.Component.Params.Output[idx].AddVolatileData(gh.Kernel.Data.GH_Path(0), 0, self._var_output[idx]) - self._var_output = ["None"] + ghenv.Component.Params.Output[idx].VolatileData.Clear() + ghenv.Component.Params.Output[idx].AddVolatileData(gh.Kernel.Data.GH_Path(0), 0, self._var_output[idx]) + self._var_output.clear() \ No newline at end of file diff --git a/GH/PyGH/components/scriptsynccpy/metadata.json b/GH/PyGH/components/scriptsynccpy/metadata.json index bed1049..fc2de1c 100644 --- a/GH/PyGH/components/scriptsynccpy/metadata.json +++ b/GH/PyGH/components/scriptsynccpy/metadata.json @@ -40,9 +40,9 @@ ], "outputParameters": [ { - "name": "out", - "nickname": "out", - "description": "The execution information, as output and error streams", + "name": "stdout", + "nickname": "stdout", + "description": "The redirected standard output of the component scriptsync.", "optional": false, "sourceCount": 0 }, diff --git a/GH/PyGH/scriptsyncGH_test.py b/GH/PyGH/scriptsyncGH_test.py index d9f6a65..1727a17 100644 --- a/GH/PyGH/scriptsyncGH_test.py +++ b/GH/PyGH/scriptsyncGH_test.py @@ -88,10 +88,13 @@ def safe_exec(self, path, globals, locals): with contextlib.redirect_stdout(output): exec(code, globals, locals) locals["stdout"] = output.getvalue() + sys.stdout = sys.__stdout__ return locals except Exception as e: - err_msg = str(e) - return e + err_msg = f"script-sync::Error in the code: {str(e)}" + # TODO: here we need to send back the erro mesage to vscode + sys.stdout = sys.__stdout__ + raise Exception(err_msg) def RunScript(self, x, y): """ This method is called whenever the component has to be recalculated. """ @@ -113,12 +116,6 @@ def RunScript(self, x, y): # run the script res = self.safe_exec(self.path, globals(), locals()) - if isinstance(res, Exception): - err_msg = f"script-sync::Error in the code: {res}" - print(err_msg) - raise Exception(err_msg) - - # get the output variables defined in the script outparam = ghenv.Component.Params.Output diff --git a/GH/PyGH/test/runner_script.py b/GH/PyGH/test/runner_script.py index a438d93..92fb5cb 100644 --- a/GH/PyGH/test/runner_script.py +++ b/GH/PyGH/test/runner_script.py @@ -4,5 +4,5 @@ print(f"runner_script.py::y value: {y}") a = 422 c = x + y -b = "asd" +b = 1239 print(f"runner_script.py::a value: {a}") \ No newline at end of file