From b1b9049d533f6398b58b4925534a51527737a529 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Apr 2023 09:31:30 +0100 Subject: [PATCH 1/5] fix run app from python --- src/textual/cli/_run.py | 16 ++++++++++++++-- src/textual/widgets/_tabs.py | 4 +++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/textual/cli/_run.py b/src/textual/cli/_run.py index 782df6c177..5df239396e 100644 --- a/src/textual/cli/_run.py +++ b/src/textual/cli/_run.py @@ -17,7 +17,19 @@ EXEC_SCRIPT = Template( """\ from textual.app import App -from $MODULE import $APP as app; +try: + from $MODULE import $APP as app; +except ImportError: + from inspect import isclass + import $MODULE as module + for symbol in dir(module): + if (isclass(symbol) and issubclass(symbol, App)) or isinstance(symbol, App): + app = symbol + break + else: + raise SystemExit("Unable locate a Textual app in module '$MODULE'") from None + + if isinstance(app, App): # If we imported an app, run it app.run() @@ -123,7 +135,7 @@ def check_import(module_name: str, app_name: str) -> bool: """ try: - sys.path.insert(0, "") + sys.path.insert(0, ".") module = importlib.import_module(module_name) except ImportError as error: return False diff --git a/src/textual/widgets/_tabs.py b/src/textual/widgets/_tabs.py index acd7bce0fe..0af93be65f 100644 --- a/src/textual/widgets/_tabs.py +++ b/src/textual/widgets/_tabs.py @@ -404,7 +404,9 @@ def watch_active(self, previously_active: str, active: str) -> None: active_tab = self.query_one(f"#tabs-list > #{active}", Tab) self.query("#tabs-list > Tab.-active").remove_class("-active") active_tab.add_class("-active") - self._highlight_active(animate=previously_active != "") + self.call_after_refresh( + self._highlight_active, animate=previously_active != "" + ) self.post_message(self.TabActivated(self, active_tab)) else: underline = self.query_one(Underline) From 5c966e7dc1679f0115d6db1a45d43364daebf24d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Apr 2023 10:13:49 +0100 Subject: [PATCH 2/5] updated run --- CHANGELOG.md | 6 ++++++ docs/guide/devtools.md | 33 ++++++++++++++++++++++++++++----- src/textual/cli/_run.py | 15 ++------------- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ead1b47ffb..12913fd3b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.22.1] - 2023-04-28 + +### Fixed + +- Fixed `textual run` issue https://github.com/Textualize/textual/issues/2391 + ## [0.22.0] - 2023-04-27 ### Fixed diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index 4fbffe6cbf..af06d3777b 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -23,18 +23,41 @@ You can run Textual apps with the `run` subcommand. If you supply a path to a Py textual run my_app.py ``` -The `run` sub-command will first look for a `App` instance called `app` in the global scope of your Python file. If there is no `app`, it will create an instance of the first `App` class it finds and run that. +This will be equivalent to running `python my_app.py` from the command prompt, but will allow you to set various switches which can help you debug, such as `--dev` which enable the [Console](#console). -Alternatively, you can add the name of an `App` instance or class after a colon to run a specific app in the Python file. Here's an example: +See the `run` subcommand's help for details: ```bash -textual run my_app.py:alternative_app +textual run --help ``` -!!! note +You can also run Textual apps from a python import. +The following command would import `music.play` and run a Textual app in that module: + +```bash +textual run music.play +``` + +This assumes you have a Textual app instance called `app` in `music.play`. +If your app has a different name, you can append it after a colon: + +```bash +textual run music.play:MusicPlayerApp +``` + +!!! note: + + This works for both Textual app *instances* and *classes*. + - If the Python file contains a call to app.run() then you can launch the file as you normally would any other Python program. Running your app via `textual run` will give you access to a few Textual features such as live editing of CSS files. +### Running from commands +If your app is installed as a command line script, you can use the `-c` switch to run it. +For instance, the following will run the `textual colors` command: + +```bash +textual run -c textual colors +``` ## Live editing diff --git a/src/textual/cli/_run.py b/src/textual/cli/_run.py index 5df239396e..d7677095d4 100644 --- a/src/textual/cli/_run.py +++ b/src/textual/cli/_run.py @@ -20,15 +20,7 @@ try: from $MODULE import $APP as app; except ImportError: - from inspect import isclass - import $MODULE as module - for symbol in dir(module): - if (isclass(symbol) and issubclass(symbol, App)) or isinstance(symbol, App): - app = symbol - break - else: - raise SystemExit("Unable locate a Textual app in module '$MODULE'") from None - + raise SystemExit("Unable to import '$APP' from module '$MODULE'") from None if isinstance(app, App): # If we imported an app, run it @@ -135,7 +127,7 @@ def check_import(module_name: str, app_name: str) -> bool: """ try: - sys.path.insert(0, ".") + sys.path.insert(0, "") module = importlib.import_module(module_name) except ImportError as error: return False @@ -159,9 +151,6 @@ def exec_import( module, _colon, app = import_name.partition(":") app = app or "app" - if not check_import(module, app): - raise ExecImportError(f"Unable to import {app!r} from {import_name!r}") - script = EXEC_SCRIPT.substitute(MODULE=module, APP=app) # Compiling the script will raise a SyntaxError if there are any invalid symbols compile(script, "textual-exec", "exec") From 76cf1145f3988fdf794b694f51ecb35e55f40202 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Apr 2023 10:16:29 +0100 Subject: [PATCH 3/5] remove function --- src/textual/cli/_run.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/textual/cli/_run.py b/src/textual/cli/_run.py index d7677095d4..dc6c835b35 100644 --- a/src/textual/cli/_run.py +++ b/src/textual/cli/_run.py @@ -115,25 +115,6 @@ def exec_command( os.execvpe(command, [command, *args], environment) -def check_import(module_name: str, app_name: str) -> bool: - """Check if a symbol can be imported. - - Args: - module_name: Name of the module - app_name: Name of the app. - - Returns: - True if the app may be imported from the module. - """ - - try: - sys.path.insert(0, "") - module = importlib.import_module(module_name) - except ImportError as error: - return False - return hasattr(module, app_name) - - def exec_import( import_name: str, args: Sequence[str], environment: dict[str, str] ) -> NoReturn: From 3d9e47459e74ac0cd63449b62b31e28ae31cb123 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Apr 2023 10:23:22 +0100 Subject: [PATCH 4/5] update help --- src/textual/cli/cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index cdc8e949c1..1365f9e219 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -147,16 +147,17 @@ def _run_app( ) -> None: """Run a Textual app. - The code to run may be given as a path (ending with .py) or as a Python - import, which will load the code and run an app called "app". You may optionally - add a colon plus the class or class instance you want to run. + The app to run may be given as a path (ending with .py) which will be equivalent to running the + script with python, or as a Python import which will import and run an app called "app". + + In the case of an import, you can import and run an alternative app by appending a colon followed + by the name of the app instance or class. + Here are some examples: textual run foo.py - textual run foo.py:MyApp - textual run module.foo textual run module.foo:MyApp From 7aeae8b79955457abb38a27ae5edddfa4c0d41c5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Fri, 28 Apr 2023 10:30:01 +0100 Subject: [PATCH 5/5] doc update --- docs/guide/devtools.md | 8 ++++---- src/textual/cli/cli.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index af06d3777b..25138aa69f 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -8,7 +8,7 @@ Textual comes with a command line application of the same name. The `textual` command is a super useful tool that will help you to build apps. -Take a moment to look through the available sub-commands. There will be even more helpful tools here in the future. +Take a moment to look through the available subcommands. There will be even more helpful tools here in the future. ```bash textual --help @@ -17,13 +17,13 @@ textual --help ## Run -You can run Textual apps with the `run` subcommand. If you supply a path to a Python file it will load and run the application. +The `run` sub-command runs Textual apps. If you supply a path to a Python file it will load and run the app. ```bash textual run my_app.py ``` -This will be equivalent to running `python my_app.py` from the command prompt, but will allow you to set various switches which can help you debug, such as `--dev` which enable the [Console](#console). +This is equivalent to running `python my_app.py` from the command prompt, but will allow you to set various switches which can help you debug, such as `--dev` which enable the [Console](#console). See the `run` subcommand's help for details: @@ -45,7 +45,7 @@ If your app has a different name, you can append it after a colon: textual run music.play:MusicPlayerApp ``` -!!! note: +!!! note This works for both Textual app *instances* and *classes*. diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index 1365f9e219..0c66cf596f 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -153,7 +153,6 @@ def _run_app( In the case of an import, you can import and run an alternative app by appending a colon followed by the name of the app instance or class. - Here are some examples: textual run foo.py