Grasshopper: Tell GhPython to wait on subprocess?

Hi, I wonder if someone might have a suggestion for utilizing subprocess in a GhPython component?

Scenario:

In Grasshopper, in a GhPython component I am passing some data off to a subprocess which calls a Python3 interpreter and script to do some data processing. This works fine, and once complete it saves some new data to a CSV file on disk. This also works fine. When it is done, it outputs the file path to the newly written CSV file. So roughly like:

However: I have this second GhPython component that would like to then read in this CSV data in (using the file path provided), and then proceed to do some other stuff with it in Grasshopper.

Question:

So I am wondering: How can I best tell my second component to wait until all the CSV files are fully written before they execute? Or maybe I should be telling my first components to somehow ‘wait’ until everything is done before they resolve? I tried adding process.wait() to my subprocess function, but that didn’t seem to do the trick? I wonder if there is a better way to compose this and force everything to wait until the entire subprocess is done before moving on to the next step?


Note: No, I cannot use the new Python3 components. I have to stick with the older GhPython components for now.


Has anyone ever composed multiple components in this manner? Any thoughts or suggestions are much appreciated.

thanks!
-Ed


Environment:

  • MacOS Sequoia 15.3.1
  • Rhino 8.17.25063.13002

If your first component is not working in some asynch manner, it should not be needed.
I did something similar many times (but with c#, on windows, on rhino 7).
The component creating the .csv should not expire until the system call of file write is not finished.

Pseudocode:

var output = ElaborateData(A,B,C);
string CSVpath = “mycustompath”;
File.IO.Write(output, CSVpath);
new_csv_file_path_ = CSVpath;

Those lines of code are executed sequentially (if you are not using any asynch method).
The last line of code, outputting the csv path, should execute only after the csv file is created.

Are you getting an error?

Hi Ed, I’ve composed GHPython components that call subprocesses, and write files for each other. It’s straightforward to use subprocess.check_output.

I’m not sure what your issue is, nor how you’re creating the subprocess. But if _data is set to list mode, it should only trigger the second component once.

Hi @James_Parrott , @maje90 ,

Thanks for the suggestions. I will try out subprocess.check_output - I have not looked at that before.

I have attached here a simplified version of my actual pipeline (I think I have the main ‘flow’ implemented the same as my actual pipeline, which I think would be too confusing to repeat here in detail). In this flow, the user runs a subrprocess through a terminal to read in data from Excel (simulated in the example here with sleep(2)) then writes some data to csv file on disk. The second component reads in that CSV data and does some other work with it. In that scenario, I do get an error with the second GhPython component, since it tries to execute before the CSV file is fully written:

Runtime error (FileNotFoundException): Could not find file '/Users/em/Desktop/my_example.csv'.

Traceback:
  line 2, in script


For reference, the GhPython component code is shown below:

Component 1:

import os
import subprocess

def run_subprocess_from_shell(commands):
    # type: (list[str]) -> tuple[bytes, bytes]
    """Run a python subprocess.Popen THROUGH a MacOS terminal via a shell, using the supplied commands.

    When talking to Excel on MacOS it is necessary to run through a Terminal since Rhino
    cannot get the right 'permissions' to interact with Excel. This is a workaround and not
    required on Windows OS.

    Arguments:
    ----------
        * _commands: (List[str]): A list of the commands to pass to Popen

    Returns:
    --------
        * Tuple:
            - [0]: stdout
            - [1]: stderr
    """

    # -- Create a new PYTHONHOME to avoid the Rhino-8 issues
    CUSTOM_ENV = os.environ.copy()
    CUSTOM_ENV["PYTHONHOME"] = ""

    use_shell = True if os.name == "nt" else False

    # -- Make sure the files are executable
    shell_file = commands[0]
    try:
        subprocess.check_call(["chmod", "u+x", shell_file])
    except Exception as e:
        print("Failed to make the shell file executable: {}".format(e))
        raise e

    python_script_path = commands[3]
    try:
        subprocess.check_call(["chmod", "u+x", python_script_path])
    except Exception as e:
        print("Failed to make the python script executable: {}".format(e))
        raise e

    process = subprocess.Popen(
        commands,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        shell=use_shell,
        env=CUSTOM_ENV,
    )
    process.wait()  # Ensure the process is completely finished
    stdout, stderr = process.communicate()

    return stdout, stderr


def run_on_macos(_shell_script, _python_3_script, _python_3_exe, _output_file):
    # type: (str, str, str, str) -> str
    """Run the subprocess through a shell-script on MacOS
    
    Runs in a 'terminal' in order to connect to Excel.
    This is a workaround for the permissions issue on MacOS.
    See:
    https://discourse.mcneel.com/t/python-subprocess-permissions-error-on-mac-os-1743/142830/6
    """
    execution_root = ""

    # -- Build up the commands to run
    commands = [
        _shell_script,  # -------------- The shell script to run
        execution_root,
        _python_3_exe,  # ---------------- The python3-interpreter to use
        _python_3_script,  # ------------- The python3-script to run
        _output_file,  # ----------------- The output CSV filepath
    ]
    stdout, stderr = run_subprocess_from_shell(commands)

    return _output_file


def run(_shell_script, _python_3_script, _python_3_exe, _output_folder):
    # type: (str, str, str, str) -> str
    if os.name == "nt":
        raise NotImplementedError("This component is not yet implemented for Windows.")
    else:
        output_file = os.path.join(_output_folder, "my_example.csv")
        output_file = run_on_macos(_shell_script, _python_3_script, _python_3_exe, output_file)
        return output_file

if _run:
    result_csv_file_ = run(_shell_script, _python_3_script, _python_3_exe, _output_folder)

Component 2:

if _run:
    with open(_result_csv_file) as f:
        print "Running Step 2..."

I know the component 1 may look overly complicated for this simplified scenario, but in my actual pipeline the functions shown are used by several different components and are distributed over a couple different packages. I’ve tried to copy/paste the relevant bits here though.

thanks!!
@ed.p.may


Example Files:

Thanks Ed. I think the issue is the boolean switch component triggers component 2 at the same time as it triggers component 1. I also return a boolean (called OK) from component 1, and connect that into _run on component 2 (which I call Go…), so it only runs once component 1 has finished and returned OK=True.

2 Likes

ah! :man_facepalming: that’s it! Thanks @James_Parrott

-e

1 Like