Skip to content

Pass Additional Information Into App #1030

@juftin

Description

@juftin

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"]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions