Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass Additional Information Into App #1030

Closed
juftin opened this issue Oct 27, 2022 · 4 comments
Closed

Pass Additional Information Into App #1030

juftin opened this issue Oct 27, 2022 · 4 comments

Comments

@juftin
Copy link
Contributor

juftin commented Oct 27, 2022

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"]
@davep
Copy link
Contributor

davep commented Oct 27, 2022

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 added a commit to davep/textual-sandbox that referenced this issue Oct 28, 2022
@davep davep closed this as completed Oct 28, 2022
@github-actions
Copy link

Did we solve your problem?

Glad we could help!

@juftin
Copy link
Contributor Author

juftin commented Oct 28, 2022

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

@davep
Copy link
Contributor

davep commented Oct 28, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants