textual icon indicating copy to clipboard operation
textual copied to clipboard

Pass Additional Information Into App

Open juftin opened this issue 3 years ago • 1 comments

TLDR; As an app developer, I'd like to pass additional information into an App instance to allow for some more more complex configuration. This originally came up when I was working on writing a CLI that embeds different textual applications under different commands and argv became complicated. I'm thinking about the implementation as an additional parameter, like config_object, that sets an instance attribute to be used downstream.



Let's say we have this nice app, simple_app.py. We can see that it prints a message depending on what arguments are passed to the python script.

from sys import argv

from textual.app import App, ComposeResult
from textual.widgets import Static


class SimplestApp(App):

    default_response = "Hey, I'm a default response since nothing was passed"

    def compose(self) -> ComposeResult:
        response = self.default_response if len(argv) == 1 else " ".join(argv[1:])
        yield Static(response)


if __name__ == "__main__":
    app = SimplestApp()
    app.run()
$ python simple_app.py
>> TUI["Hey, I'm a default response since nothing was passed"]
$ python simple_app.py Print Something Else
>> TUI["Print Something Else"]

But now, I want to import this app into another file and nest it under a command line application, command_line.py:

from typing import Tuple

import click

from simple_app import SimplestApp


@click.group()
def cli():
    pass


@cli.command("textual-app")
def textual_app(args: Tuple[str]):
    SimplestApp().run()


if __name__ == "__main__":
    cli()
$ python command_line.py textual-app
>> TUI["textual-app"]
$ python command_line.py textual-app Print Something Entirely Different
>> TUI["textual-app Print Something Entirely Different"]

argv becomes a little complicated to manage when being nested on other CLIs. Instead, it would be awesome to do something like this to pass in a more complex configuration object:

@cli.command("textual-app")
@click.argument("args", nargs=-1, required=False)
def textual_app(args: List[str]):
    SimplestApp(config_object={"args": args}).run()

and subsequently be able to grab that config like this:

def compose(self) -> ComposeResult:
    assert isinstance(self.config_object, dict)
    assert "args" in self.config_object
    response = (
        " ".join(self.config_object["args"])
        if self.config_object["args"]
        else self.default_response
    )
    yield Static(response)


Here's how I've implemented this myself using a subclass of App - let me know your thoughts. I'd be more than happy to contribute.

from __future__ import annotations

from sys import argv
from typing import Type, Any

from textual.app import App, ComposeResult, CSSPathType
from textual.driver import Driver
from textual.widgets import Static


class AppWithConfig(App):
    def __init__(
        self,
        driver_class: Type[Driver] | None = None,
        css_path: CSSPathType = None,
        watch_css: bool = False,
        config_object: Any = None,
    ):
        """
        Like the textual.app.App class, but with an extra config_object property

        Parameters
        ----------
        driver_class: Type[Driver]
        css_path: CSSPathType
        watch_css: bool
        config_object: Any
            A configuration object. This is an optional python object,
            like a dictionary to pass into an application
        """
        self.config_object = config_object
        super().__init__(
            driver_class=driver_class, css_path=css_path, watch_css=watch_css
        )


class SimplestApp(AppWithConfig):

    default_response = "Hey, I'm a default response since nothing was passed"

    def compose(self) -> ComposeResult:
        assert isinstance(self.config_object, dict)
        assert "args" in self.config_object
        response = (
            " ".join(self.config_object["args"])
            if self.config_object["args"]
            else self.default_response
        )
        yield Static(response)


if __name__ == "__main__":
    args = argv[1:]
    app = SimplestApp(config_object={"args": args})
    app.run()

... and now:

$ python command_line.py textual-app
>> TUI["Hey, I'm a default response since nothing was passed"]
$ python command_line.py textual-app Print Something Entirely Different
>> TUI["Print Something Entirely Different"]
$ python simple_app.py
>> TUI["Hey, I'm a default response since nothing was passed"]
$ python simple_app.py Print Something Else
>> TUI["Print Something Else"]

juftin avatar Oct 27 '22 03:10 juftin

Given that a Textual app is always going to be done as a subclass of App, I'd suggest that the implementation you've done above is how you'd do it: implement your own __init__ such that it takes the extra parameters, etc, that your app needs, and go from there; either done as a single structure like you've done above, or simply by using specific parameters. For example:

from textual.app import App, ComposeResult
from textual.widgets import Static

class Greetings(App[None]):

    def __init__(self, greeting: str="Hello", to_greet: str="World") -> None:
        self.greeting = greeting
        self.to_greet = to_greet
        super().__init__()

    def compose(self) -> ComposeResult:
        yield Static(f"{self.greeting}, {self.to_greet}")

if __name__=="__main__":
    Greetings().run()
    Greetings(to_greet="davep").run()
    Greetings("Well hello", "there").run()

davep avatar Oct 27 '22 08:10 davep

Did we solve your problem?

Glad we could help!

github-actions[bot] avatar Oct 28 '22 11:10 github-actions[bot]

Thanks Dave and Textual Team, Textual has been awesome to work with so far!

juftin avatar Oct 28 '22 15:10 juftin

My pleasure; glad to hear you're enjoying it. :-)

davep avatar Oct 28 '22 17:10 davep