Skip to content

Commit

Permalink
CAP-WIP: modifiedd functioning CI componentizer
Browse files Browse the repository at this point in the history
  • Loading branch information
9and3 committed Jan 28, 2024
1 parent 35e8328 commit 44be04e
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 93 deletions.
12 changes: 0 additions & 12 deletions .github/actions/ghpython-components/componentize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
Expand Down Expand Up @@ -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"])
Expand Down
175 changes: 106 additions & 69 deletions GH/PyGH/components/scriptsynccpy/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
6 changes: 3 additions & 3 deletions GH/PyGH/components/scriptsynccpy/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
13 changes: 5 additions & 8 deletions GH/PyGH/scriptsyncGH_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. """
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion GH/PyGH/test/runner_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

0 comments on commit 44be04e

Please sign in to comment.