marimo icon indicating copy to clipboard operation
marimo copied to clipboard

Formatting of stdout stream

Open TobiasEnergyMachines opened this issue 1 year ago • 2 comments

Description

Hi Marimo team

Thanks for the fix for "plotly mapbox styles".

I am running an executable using subprocess.Popen and would like to stream the stdout "live" (the process sometimes run up to minutes and I would like to see the stdout stream as soon as it is generated from the executable) . I have attempted to use https://docs.marimo.io/api/outputs.html#marimo.redirect_stdout but I find it limiting since:

-I am not able to stream the stdout into a text-field or accordion (would like to get the stdout stream into a tab). -I am not able to limit the length of the output view in app mode (like there is limit to cell output in edit mode).

Would you be able to help with this?

Best regards Tobias

Example

Example of capturing subprocess stdout with marimo.redirect_stdout(). This showcases current limitations:


import marimo

__generated_with = "0.2.0"
app = marimo.App()


@app.cell
def __():
    import marimo as mo
    from subprocess import PIPE, Popen
    return PIPE, Popen, mo


@app.cell
def __(mo):
    button = mo.ui.button(
        label="Run/Reset subprocess", value=False, on_click=lambda value: not value)

    button
    return button,


@app.cell
def __(PIPE, Popen, button, mo):
    mo.stop(button.value == False)

    with mo.redirect_stdout():

        args = ["timeout", "20"]

        with Popen(args, stdout=PIPE) as p:
            while True:
                text = p.stdout.read1().decode("utf-8")  
                print(text, end="", flush=True)
                if p.poll() is not None:  
                    break
    return args, p, text


if __name__ == "__main__":
    app.run()

Alternative

No response

Additional context

No response

TobiasEnergyMachines avatar Feb 02 '24 11:02 TobiasEnergyMachines

hi @TobiasEnergyMachines, you might find better success using mo.capture_stdout() to capture the request and then mo.output.append() or mo.output.replace() (outputs)

Can you give those a try (referencing the docs) and let us know if something could be improved?

mscolnick avatar Feb 02 '24 17:02 mscolnick

Thank you for the suggestions. I have tried to use mo.capture_stdout() together with mo.output.append() and mo.output.replace(). In both cases I find that the output from the subprocess is only printed after completion of the subprocess (i.e. blocking). This can be verified from this example:

import marimo

__generated_with = "0.2.1"
app = marimo.App()


@app.cell
def __():
    import marimo as mo
    from subprocess import Popen, PIPE
    return PIPE, Popen, mo


@app.cell
def __(PIPE, Popen):
    def run_args(args: list):
        output = ""
        with Popen(args, stdout=PIPE) as p:
            while True:
                text = p.stdout.read1().decode("utf-8")
                print(text, end="", flush=True)
                output += text
                if p.poll() is not None:
                    break
        return output
    return run_args,


@app.cell
def __():
    args = ["timeout", "5"] # "placeholder for executable" subprocess running for 5 seconds
    return args,


@app.cell
def __(mo):
    button = mo.ui.button(
        label="Run/Reset subprocess", value=False, on_click=lambda value: not value)
    button
    return button,


@app.cell
def __(mo):
    mo.md("## Using mo.output.replace()")
    return


@app.cell
def __(args, button, mo, run_args):
    if button.value:
        with mo.capture_stdout() as buffer1:
            run_args(args)
            mo.output.replace(buffer1.getvalue())
    return buffer1,


@app.cell
def __(mo):
    mo.md("## Using mo.output.append()")
    return


@app.cell
def __(args, button, mo, run_args):
    if button.value:
        with mo.capture_stdout() as buffer2:
            run_args(args)
            mo.output.append(buffer2.getvalue())
    return buffer2,


if __name__ == "__main__":
    app.run()

The closest thing I got to a solution is the following example. The issue I have with this solution is that it is overriding the cell output when it is streaming the subprocess output. Also it is resetting the tab selection which is not ideal:

import marimo

__generated_with = "0.2.1"
app = marimo.App()


@app.cell
def __():
    import marimo as mo
    from subprocess import Popen, PIPE
    return PIPE, Popen, mo


@app.cell
def __(PIPE, Popen):
    def run_args(args: list):
        output = ""
        with Popen(args, stdout=PIPE) as p:
            while True:
                text = p.stdout.read1().decode("utf-8")
                print(text, end="", flush=True)
                output += text
                if p.poll() is not None:
                    break
        return output
    return run_args,


@app.cell
def __():
    args = ["timeout", "5"] # "placeholder for executable" subprocess running for 5 seconds
    return args,


@app.cell
def __(mo):
    button = mo.ui.button(
        label="Run/Reset subprocess", value=False, on_click=lambda value: not value)
    return button,


@app.cell
def __(mo, run_args):
    def stream_and_save_output(_run_, _args_):
        if _run_:
            with mo.redirect_stdout():
                output = run_args(_args_)
                return output
        else:
            return "Press run to stream output!"
    return stream_and_save_output,


@app.cell
def __(args, button, mo, stream_and_save_output):
    mo.tabs({
        "Inputs": mo.plain_text("Provide simulation inputs"),
        "Simulate": mo.vstack(items=[
            button, 
            stream_and_save_output(button.value, args)
        ]),
        "Analyze": mo.plain_text("Analyze simulation outputs"),

    })
    return


if __name__ == "__main__":
    app.run()

Best regards Tobias

TobiasEnergyMachines avatar Feb 05 '24 13:02 TobiasEnergyMachines