Pass Additional Information Into App
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"]
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()
Did we solve your problem?
Glad we could help!
Thanks Dave and Textual Team, Textual has been awesome to work with so far!
My pleasure; glad to hear you're enjoying it. :-)