mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
New CLI runner (#2338)
* New CLI runner * runner functionality * Add port * use env for port * changelog * test fix * flush * remove constant * comment * tidy docs * docstrings * punctuation * docstring * fix test * snapshot * Update src/textual/cli/cli.py Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * guard against bad imports * guard againsts screenshot * always print return * docstrings * docstrings --------- Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com>
This commit is contained in:
@@ -9,17 +9,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Changed
|
||||
|
||||
- Breaking change: standard keyboard scrollable navigation bindings have been moved off `Widget` and onto a new base class for scrollable containers (see also below addition) https://github.com/Textualize/textual/issues/2332
|
||||
- `ScrollView` now inherits from `ScrollableContainer` rather than `Widget` https://github.com/Textualize/textual/issues/2332
|
||||
- Containers no longer inherit any bindings from `Widget` https://github.com/Textualize/textual/issues/2331
|
||||
- `textual run` execs apps in a new context.
|
||||
|
||||
### Added
|
||||
|
||||
- Added `-c` switch to `textual run` which runs commands in a Textual dev environment.
|
||||
- Breaking change: standard keyboard scrollable navigation bindings have been moved off `Widget` and onto a new base class for scrollable containers (see also below addition) https://github.com/Textualize/textual/issues/2332
|
||||
- `ScrollView` now inherits from `ScrollableContainer` rather than `Widget` https://github.com/Textualize/textual/issues/2332
|
||||
- Containers no longer inherit any bindings from `Widget` https://github.com/Textualize/textual/issues/2331
|
||||
- Added `ScrollableContainer`; a container class that binds the common navigation keys to scroll actions (see also above breaking change) https://github.com/Textualize/textual/issues/2332
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed dark mode toggles in a "child" screen not updating a "parent" screen https://github.com/Textualize/textual/issues/1999
|
||||
- Fixed "panel" border not exposed via CSS
|
||||
|
||||
## [0.20.1] - 2023-04-18
|
||||
|
||||
|
||||
@@ -1048,9 +1048,19 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
auto_pilot_task: Task | None = None
|
||||
|
||||
if auto_pilot is None and constants.PRESS:
|
||||
keys = constants.PRESS.split(",")
|
||||
|
||||
async def press_keys(pilot: Pilot) -> None:
|
||||
"""Auto press keys."""
|
||||
await pilot.press(*keys)
|
||||
|
||||
auto_pilot = press_keys
|
||||
|
||||
async def app_ready() -> None:
|
||||
"""Called by the message loop when the app is ready."""
|
||||
nonlocal auto_pilot_task
|
||||
|
||||
if auto_pilot is not None:
|
||||
|
||||
async def run_auto_pilot(
|
||||
@@ -1762,23 +1772,16 @@ class App(Generic[ReturnType], DOMNode):
|
||||
May be used as a hook for any operations that should run first.
|
||||
|
||||
"""
|
||||
try:
|
||||
screenshot_timer = float(os.environ.get("TEXTUAL_SCREENSHOT", "0"))
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
screenshot_title = os.environ.get("TEXTUAL_SCREENSHOT_TITLE")
|
||||
|
||||
if not screenshot_timer:
|
||||
return
|
||||
|
||||
async def on_screenshot():
|
||||
"""Used by docs plugin."""
|
||||
svg = self.export_screenshot(title=screenshot_title)
|
||||
self._screenshot = svg # type: ignore
|
||||
async def take_screenshot() -> None:
|
||||
"""Take a screenshot and exit."""
|
||||
self.save_screenshot()
|
||||
self.exit()
|
||||
|
||||
self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer")
|
||||
if constants.SCREENSHOT_DELAY >= 0:
|
||||
self.set_timer(
|
||||
constants.SCREENSHOT_DELAY, take_screenshot, name="screenshot timer"
|
||||
)
|
||||
|
||||
async def _on_compose(self) -> None:
|
||||
try:
|
||||
@@ -1966,6 +1969,14 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
self._print_error_renderables()
|
||||
|
||||
if constants.SHOW_RETURN:
|
||||
from rich.console import Console
|
||||
from rich.pretty import Pretty
|
||||
|
||||
console = Console()
|
||||
console.print("[b]The app returned:")
|
||||
console.print(Pretty(self._return_value))
|
||||
|
||||
async def _on_exit_app(self) -> None:
|
||||
self._begin_batch() # Prevent repaint / layout while shutting down
|
||||
await self._message_queue.put(None)
|
||||
|
||||
163
src/textual/cli/_run.py
Normal file
163
src/textual/cli/_run.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
|
||||
Functions to run Textual apps with an updated environment.
|
||||
|
||||
Note that these methods will execute apps in a new process, and abandon the current process.
|
||||
This means that (if they succeed) they will never return.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import platform
|
||||
import shlex
|
||||
import sys
|
||||
from string import Template
|
||||
from typing import NoReturn
|
||||
|
||||
WINDOWS = platform.system() == "Windows"
|
||||
"""True if we're running on Windows."""
|
||||
|
||||
EXEC_SCRIPT = Template(
|
||||
"""\
|
||||
from textual.app import App
|
||||
from $MODULE import $APP as app;
|
||||
if isinstance(app, App):
|
||||
# If we imported an app, run it
|
||||
app.run()
|
||||
else:
|
||||
# Otherwise it is assumed to be a class
|
||||
app().run()
|
||||
"""
|
||||
)
|
||||
"""A template script to import and run an app."""
|
||||
|
||||
|
||||
class ExecImportError(Exception):
|
||||
"""Raised if a Python import is invalid."""
|
||||
|
||||
|
||||
def run_app(command_args: str, environment: dict[str, str] | None = None) -> None:
|
||||
"""Run a textual app.
|
||||
|
||||
Note:
|
||||
The current process is abandoned.
|
||||
|
||||
Args:
|
||||
command_args: Arguments to pass to the Textual app.
|
||||
environment: Environment variables, or None to use current process.
|
||||
"""
|
||||
import_name, *args = shlex.split(command_args, posix=not WINDOWS)
|
||||
if environment is None:
|
||||
app_environment = dict(os.environ)
|
||||
else:
|
||||
app_environment = environment
|
||||
|
||||
if _is_python_path(import_name):
|
||||
# If it is a Python path we can exec it now
|
||||
exec_python([import_name, *args], app_environment)
|
||||
|
||||
else:
|
||||
# Otherwise it is assumed to be a Python import
|
||||
try:
|
||||
exec_import(import_name, args, app_environment)
|
||||
except (SyntaxError, ExecImportError):
|
||||
print(f"Unable to import Textual app from {import_name!r}")
|
||||
|
||||
|
||||
def _is_python_path(path: str) -> bool:
|
||||
"""Does the given file look like it's run with Python?
|
||||
|
||||
Args:
|
||||
path: The path to check.
|
||||
|
||||
Returns:
|
||||
True if the path references Python code, False it it doesn't.
|
||||
"""
|
||||
if path.endswith(".py"):
|
||||
return True
|
||||
try:
|
||||
with open(path, "r") as source:
|
||||
first_line = source.readline()
|
||||
except IOError:
|
||||
return False
|
||||
return first_line.startswith("#!") and "py" in first_line
|
||||
|
||||
|
||||
def _flush() -> None:
|
||||
"""Flush output before exec."""
|
||||
sys.stderr.flush()
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def exec_python(args: list[str], environment: dict[str, str]) -> NoReturn:
|
||||
"""Execute a Python script.
|
||||
|
||||
Args:
|
||||
args: Additional arguments.
|
||||
environment: Environment variables.
|
||||
|
||||
"""
|
||||
_flush()
|
||||
os.execvpe(sys.executable, ["python", *args], environment)
|
||||
|
||||
|
||||
def exec_command(command: str, environment: dict[str, str]) -> NoReturn:
|
||||
"""Execute a command with the given environment.
|
||||
|
||||
Args:
|
||||
command: Command to execute.
|
||||
environment: Environment variables.
|
||||
"""
|
||||
_flush()
|
||||
command, *args = shlex.split(command, posix=not WINDOWS)
|
||||
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: list[str], environment: dict[str, str]
|
||||
) -> NoReturn:
|
||||
"""Import and execute an app.
|
||||
|
||||
Raises:
|
||||
SyntaxError: If any imports are not valid Python symbols.
|
||||
ExecImportError: If the module could not be imported.
|
||||
|
||||
Args:
|
||||
import_name: The Python import.
|
||||
args: Command line arguments.
|
||||
environment: Environment variables.
|
||||
|
||||
"""
|
||||
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")
|
||||
_flush()
|
||||
exec_python(["-c", script, *args], environment)
|
||||
@@ -2,7 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from ..constants import DEFAULT_DEVTOOLS_PORT, DEVTOOLS_PORT_ENVIRON_VARIABLE
|
||||
from ..constants import DEVTOOLS_PORT
|
||||
from ._run import exec_command, run_app
|
||||
|
||||
try:
|
||||
import click
|
||||
@@ -12,9 +13,6 @@ except ImportError:
|
||||
|
||||
from importlib_metadata import version
|
||||
|
||||
from textual._import_app import AppFail, import_app
|
||||
from textual.pilot import Pilot
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version("textual"))
|
||||
@@ -29,31 +27,27 @@ def run():
|
||||
type=int,
|
||||
default=None,
|
||||
metavar="PORT",
|
||||
help=f"Port to use for the development mode console. Defaults to {DEFAULT_DEVTOOLS_PORT}.",
|
||||
help=f"Port to use for the development mode console. Defaults to {DEVTOOLS_PORT}.",
|
||||
)
|
||||
@click.option("-v", "verbose", help="Enable verbose logs.", is_flag=True)
|
||||
@click.option("-x", "--exclude", "exclude", help="Exclude log group(s)", multiple=True)
|
||||
def console(port: int | None, verbose: bool, exclude: list[str]) -> None:
|
||||
"""Launch the textual console."""
|
||||
import os
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from textual.devtools.server import _run_devtools
|
||||
|
||||
if port is not None:
|
||||
os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port)
|
||||
|
||||
console = Console()
|
||||
console.clear()
|
||||
console.show_cursor(False)
|
||||
try:
|
||||
_run_devtools(verbose=verbose, exclude=exclude)
|
||||
_run_devtools(verbose=verbose, exclude=exclude, port=port)
|
||||
finally:
|
||||
console.show_cursor(True)
|
||||
|
||||
|
||||
def _post_run_warnings() -> None:
|
||||
def _pre_run_warnings() -> None:
|
||||
"""Look for and report any issues with the environment.
|
||||
|
||||
This is the right place to add code that looks at the terminal, or other
|
||||
@@ -73,8 +67,10 @@ def _post_run_warnings() -> None:
|
||||
# second item is a message to show the user on exit from `textual run`.
|
||||
warnings = [
|
||||
(
|
||||
platform.system() == "Darwin"
|
||||
and os.environ.get("TERM_PROGRAM") == "Apple_Terminal",
|
||||
(
|
||||
platform.system() == "Darwin"
|
||||
and os.environ.get("TERM_PROGRAM") == "Apple_Terminal"
|
||||
),
|
||||
"The default terminal app on macOS is limited to 256 colors. See our FAQ for more details:\n\n"
|
||||
"https://github.com/Textualize/textual/blob/main/FAQ.md#why-doesn't-textual-look-good-on-macos",
|
||||
)
|
||||
@@ -82,7 +78,11 @@ def _post_run_warnings() -> None:
|
||||
|
||||
for concerning, concern in warnings:
|
||||
if concerning:
|
||||
console.print(Panel.fit(f"⚠️ [bold green] {concern}[/]", style="cyan"))
|
||||
console.print(
|
||||
Panel.fit(
|
||||
f"⚠️ {concern}", style="yellow", border_style="red", padding=(1, 2)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@run.command(
|
||||
@@ -92,25 +92,51 @@ def _post_run_warnings() -> None:
|
||||
},
|
||||
)
|
||||
@click.argument("import_name", metavar="FILE or FILE:APP")
|
||||
@click.option("--dev", "dev", help="Enable development mode", is_flag=True)
|
||||
@click.option("--dev", "dev", help="Enable development mode.", is_flag=True)
|
||||
@click.option(
|
||||
"--port",
|
||||
"port",
|
||||
type=int,
|
||||
default=None,
|
||||
metavar="PORT",
|
||||
help=f"Port to use for the development mode console. Defaults to {DEFAULT_DEVTOOLS_PORT}.",
|
||||
help=f"Port to use for the development mode console. Defaults to {DEVTOOLS_PORT}.",
|
||||
)
|
||||
@click.option(
|
||||
"--press", "press", default=None, help="Comma separated keys to simulate press."
|
||||
)
|
||||
@click.option("--press", "press", help="Comma separated keys to simulate press")
|
||||
@click.option(
|
||||
"--screenshot",
|
||||
type=int,
|
||||
default=None,
|
||||
metavar="DELAY",
|
||||
help="Take screenshot after DELAY seconds",
|
||||
help="Take screenshot after DELAY seconds.",
|
||||
)
|
||||
def run_app(
|
||||
import_name: str, dev: bool, port: int | None, press: str, screenshot: int | None
|
||||
@click.option(
|
||||
"-c",
|
||||
"--command",
|
||||
"command",
|
||||
type=bool,
|
||||
default=False,
|
||||
help="Run as command rather that a file / module.",
|
||||
is_flag=True,
|
||||
)
|
||||
@click.option(
|
||||
"-r",
|
||||
"--show-return",
|
||||
"show_return",
|
||||
type=bool,
|
||||
default=False,
|
||||
help="Show any return value on exit.",
|
||||
is_flag=True,
|
||||
)
|
||||
def _run_app(
|
||||
import_name: str,
|
||||
dev: bool,
|
||||
port: int | None,
|
||||
press: str | None,
|
||||
screenshot: int | None,
|
||||
command: bool = False,
|
||||
show_return: bool = False,
|
||||
) -> None:
|
||||
"""Run a Textual app.
|
||||
|
||||
@@ -132,53 +158,39 @@ def run_app(
|
||||
in quotes:
|
||||
|
||||
textual run "foo.py arg --option"
|
||||
|
||||
Use the -c switch to run a command that launches a Textual app.
|
||||
|
||||
textual run -c "textual colors"
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from asyncio import sleep
|
||||
|
||||
from textual.features import parse_features
|
||||
|
||||
if port is not None:
|
||||
os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port)
|
||||
environment = dict(os.environ)
|
||||
|
||||
features = set(parse_features(os.environ.get("TEXTUAL", "")))
|
||||
features = set(parse_features(environment.get("TEXTUAL", "")))
|
||||
if dev:
|
||||
features.add("debug")
|
||||
features.add("devtools")
|
||||
|
||||
os.environ["TEXTUAL"] = ",".join(sorted(features))
|
||||
try:
|
||||
app = import_app(import_name)
|
||||
except AppFail as error:
|
||||
from rich.console import Console
|
||||
environment["TEXTUAL"] = ",".join(sorted(features))
|
||||
if port is not None:
|
||||
environment["TEXTUAL_DEVTOOLS_PORT"] = str(port)
|
||||
if press is not None:
|
||||
environment["TEXTUAL_PRESS"] = str(press)
|
||||
if screenshot is not None:
|
||||
environment["TEXTUAL_SCREENSHOT"] = str(screenshot)
|
||||
if show_return:
|
||||
environment["TEXTUAL_SHOW_RETURN"] = "1"
|
||||
|
||||
console = Console(stderr=True)
|
||||
console.print(str(error))
|
||||
sys.exit(1)
|
||||
_pre_run_warnings()
|
||||
|
||||
press_keys = press.split(",") if press else None
|
||||
|
||||
async def run_press_keys(pilot: Pilot) -> None:
|
||||
if press_keys is not None:
|
||||
await pilot.press(*press_keys)
|
||||
if screenshot is not None:
|
||||
await sleep(screenshot)
|
||||
filename = pilot.app.save_screenshot()
|
||||
pilot.app.exit(message=f"Saved {filename!r}")
|
||||
|
||||
result = app.run(auto_pilot=run_press_keys)
|
||||
|
||||
if result is not None:
|
||||
from rich.console import Console
|
||||
from rich.pretty import Pretty
|
||||
|
||||
console = Console()
|
||||
console.print("[b]The app returned:")
|
||||
console.print(Pretty(result))
|
||||
|
||||
_post_run_warnings()
|
||||
if command:
|
||||
exec_command(import_name, environment)
|
||||
else:
|
||||
run_app(import_name, environment)
|
||||
|
||||
|
||||
@run.command("borders")
|
||||
@@ -215,7 +227,7 @@ def keys():
|
||||
|
||||
@run.command("diagnose")
|
||||
def run_diagnose():
|
||||
"""Print information about the Textual environment"""
|
||||
"""Print information about the Textual environment."""
|
||||
from textual.cli.tools.diagnose import diagnose
|
||||
|
||||
diagnose()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.constants import BORDERS
|
||||
from textual.containers import VerticalScroll
|
||||
from textual.css.constants import VALID_BORDER
|
||||
from textual.widgets import Button, Label
|
||||
|
||||
TEXT = """I must not fear.
|
||||
@@ -26,7 +26,7 @@ class BorderButtons(VerticalScroll):
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
for border in BORDERS:
|
||||
for border in sorted(VALID_BORDER):
|
||||
if border:
|
||||
yield Button(border, id=border)
|
||||
|
||||
|
||||
5
src/textual/cli/run.py
Normal file
5
src/textual/cli/run.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import sys
|
||||
|
||||
from ._run import run
|
||||
|
||||
run(sys.argv[1], sys.argv[1:])
|
||||
@@ -9,10 +9,6 @@ import os
|
||||
|
||||
from typing_extensions import Final
|
||||
|
||||
from ._border import BORDER_CHARS
|
||||
|
||||
__all__ = ["BORDERS"]
|
||||
|
||||
get_environ = os.environ.get
|
||||
|
||||
|
||||
@@ -42,16 +38,18 @@ def get_environ_int(name: str, default: int) -> int:
|
||||
or the default value otherwise.
|
||||
"""
|
||||
try:
|
||||
return int(get_environ(name, default))
|
||||
return int(os.environ[name])
|
||||
except KeyError:
|
||||
return default
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
BORDERS = list(BORDER_CHARS)
|
||||
|
||||
DEBUG: Final[bool] = get_environ_bool("TEXTUAL_DEBUG")
|
||||
"""Enable debug mode."""
|
||||
|
||||
DRIVER: Final[str | None] = get_environ("TEXTUAL_DRIVER", None)
|
||||
"""Import for replacement driver."""
|
||||
|
||||
FILTERS: Final[str] = get_environ("TEXTUAL_FILTERS", "")
|
||||
"""A list of filters to apply to renderables."""
|
||||
@@ -59,12 +57,14 @@ FILTERS: Final[str] = get_environ("TEXTUAL_FILTERS", "")
|
||||
LOG_FILE: Final[str | None] = get_environ("TEXTUAL_LOG", None)
|
||||
"""A last resort log file that appends all logs, when devtools isn't working."""
|
||||
|
||||
|
||||
DEVTOOLS_PORT_ENVIRON_VARIABLE: Final[str] = "TEXTUAL_CONSOLE_PORT"
|
||||
"""The name of the environment variable that sets the port for the devtools."""
|
||||
DEFAULT_DEVTOOLS_PORT: Final[int] = 8081
|
||||
"""The default port to use for the devtools."""
|
||||
DEVTOOLS_PORT: Final[int] = get_environ_int(
|
||||
DEVTOOLS_PORT_ENVIRON_VARIABLE, DEFAULT_DEVTOOLS_PORT
|
||||
)
|
||||
DEVTOOLS_PORT: Final[int] = get_environ_int("TEXTUAL_DEVTOOLS_PORT", 8081)
|
||||
"""Constant with the port that the devtools will connect to."""
|
||||
|
||||
SCREENSHOT_DELAY: Final[int] = get_environ_int("TEXTUAL_SCREENSHOT", -1)
|
||||
"""Seconds delay before taking screenshot."""
|
||||
|
||||
PRESS: Final[str] = get_environ("TEXTUAL_PRESS", "")
|
||||
"""Keys to automatically press."""
|
||||
|
||||
SHOW_RETURN: Final[bool] = get_environ_bool("TEXTUAL_SHOW_RETURN")
|
||||
"""Write the return value on exit."""
|
||||
|
||||
@@ -20,6 +20,7 @@ VALID_BORDER: Final = {
|
||||
"inner",
|
||||
"none",
|
||||
"outer",
|
||||
"panel",
|
||||
"round",
|
||||
"solid",
|
||||
"tall",
|
||||
|
||||
@@ -8,8 +8,9 @@ from aiohttp.web_request import Request
|
||||
from aiohttp.web_routedef import get
|
||||
from aiohttp.web_ws import WebSocketResponse
|
||||
|
||||
from textual.devtools.client import DEVTOOLS_PORT
|
||||
from textual.devtools.service import DevtoolsService
|
||||
from ..constants import DEVTOOLS_PORT
|
||||
from .client import DEVTOOLS_PORT
|
||||
from .service import DevtoolsService
|
||||
|
||||
DEFAULT_SIZE_CHANGE_POLL_DELAY_SECONDS = 2
|
||||
|
||||
@@ -38,7 +39,9 @@ async def _on_startup(app: Application) -> None:
|
||||
await service.start()
|
||||
|
||||
|
||||
def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None:
|
||||
def _run_devtools(
|
||||
verbose: bool, exclude: list[str] | None = None, port: int | None = None
|
||||
) -> None:
|
||||
app = _make_devtools_aiohttp_app(verbose=verbose, exclude=exclude)
|
||||
|
||||
def noop_print(_: str) -> None:
|
||||
@@ -47,7 +50,7 @@ def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None:
|
||||
try:
|
||||
run_app(
|
||||
app,
|
||||
port=DEVTOOLS_PORT,
|
||||
port=DEVTOOLS_PORT if port is None else port,
|
||||
print=noop_print,
|
||||
loop=asyncio.get_event_loop(),
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ from rich.console import ConsoleDimensions
|
||||
from rich.panel import Panel
|
||||
|
||||
from tests.utilities.render import wait_for_predicate
|
||||
from textual.constants import DEFAULT_DEVTOOLS_PORT
|
||||
from textual.constants import DEVTOOLS_PORT
|
||||
from textual.devtools.client import DevtoolsClient
|
||||
from textual.devtools.redirect_output import DevtoolsLog
|
||||
|
||||
@@ -22,7 +22,7 @@ TIMESTAMP = 1649166819
|
||||
|
||||
def test_devtools_client_initialize_defaults():
|
||||
devtools = DevtoolsClient()
|
||||
assert devtools.url == f"ws://127.0.0.1:{DEFAULT_DEVTOOLS_PORT}"
|
||||
assert devtools.url == f"ws://127.0.0.1:{DEVTOOLS_PORT}"
|
||||
|
||||
|
||||
async def test_devtools_client_is_connected(devtools):
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user