Add --port option to textual console. (#2258)

* Add --port option to textual console.

* Changelog.

* Address review feedback.

* Mark unpredictable test as xfail.

This test gets an xfail mark until #2254 is open.

* Make DEVTOOLS_PORT a constant.

Related review: https://github.com/Textualize/textual/pull/2258\#discussion_r1165210395

* Factor logic into function.

Related review: https://github.com/Textualize/textual/pull/2258\#discussion_r1165298259

* Remove dead import.
This commit is contained in:
Rodrigo Girão Serrão
2023-04-13 11:57:35 +01:00
committed by GitHub
parent e32cdbb390
commit bb2c31ba35
9 changed files with 90 additions and 11 deletions

View File

@@ -16,14 +16,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added
- Added `DataTable.remove_row` method https://github.com/Textualize/textual/pull/2253
- option `--port` to the command `textual console` to specify which port the console should connect to https://github.com/Textualize/textual/pull/2258
- `Widget.scroll_to_center` method to scroll children to the center of container widget https://github.com/Textualize/textual/pull/2255 and https://github.com/Textualize/textual/pull/2276
- Added `TabActivated` message to `TabbedContent` https://github.com/Textualize/textual/pull/2260
### Fixed
- Fixed order styles are applied in DataTable - allows combining of renderable styles and component classes https://github.com/Textualize/textual/pull/2272
## [0.19.1] - 2023-04-10
### Fixed

View File

@@ -90,6 +90,20 @@ Multiple groups may be excluded, for example to exclude everything except warnin
textual console -x SYSTEM -x EVENT -x DEBUG -x INFO
```
### Custom port
You can use the option `--port` to specify a custom port to run the console on, which comes in handy if you have other software running on the port that Textual uses by default:
```bash
textual console --port 7342
```
Then, use the command `run` with the same `--port` option:
```bash
textual run --dev --port 7342 my_app.py
```
## Textual log

View File

@@ -2,6 +2,8 @@ from __future__ import annotations
import sys
from ..constants import DEFAULT_DEVTOOLS_PORT, DEVTOOLS_PORT_ENVIRON_VARIABLE
try:
import click
except ImportError:
@@ -21,14 +23,27 @@ def run():
@run.command(help="Run the Textual Devtools console.")
@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}.",
)
@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(verbose: bool, exclude: list[str]) -> None:
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)
@@ -78,6 +93,14 @@ 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(
"--port",
"port",
type=int,
default=None,
metavar="PORT",
help=f"Port to use for the development mode console. Defaults to {DEFAULT_DEVTOOLS_PORT}.",
)
@click.option("--press", "press", help="Comma separated keys to simulate press")
@click.option(
"--screenshot",
@@ -86,7 +109,9 @@ def _post_run_warnings() -> None:
metavar="DELAY",
help="Take screenshot after DELAY seconds",
)
def run_app(import_name: str, dev: bool, press: str, screenshot: int | None) -> None:
def run_app(
import_name: str, dev: bool, port: int | None, press: str, screenshot: int | None
) -> None:
"""Run a Textual app.
The code to run may be given as a path (ending with .py) or as a Python
@@ -107,7 +132,6 @@ def run_app(import_name: str, dev: bool, press: str, screenshot: int | None) ->
in quotes:
textual run "foo.py arg --option"
"""
import os
@@ -116,6 +140,9 @@ def run_app(import_name: str, dev: bool, press: str, screenshot: int | None) ->
from textual.features import parse_features
if port is not None:
os.environ[DEVTOOLS_PORT_ENVIRON_VARIABLE] = str(port)
features = set(parse_features(os.environ.get("TEXTUAL", "")))
if dev:
features.add("debug")

View File

@@ -25,10 +25,28 @@ def get_environ_bool(name: str) -> bool:
Returns:
`True` if the env var is "1", otherwise `False`.
"""
has_environ = os.environ.get(name) == "1"
has_environ = get_environ(name) == "1"
return has_environ
def get_environ_int(name: str, default: int) -> int:
"""Retrieves an integer environment variable.
Args:
name: Name of environment variable.
default: The value to use if the value is not set, or set to something other
than a valid integer.
Returns:
The integer associated with the environment variable if it's set to a valid int
or the default value otherwise.
"""
try:
return int(get_environ(name, default))
except ValueError:
return default
BORDERS = list(BORDER_CHARS)
DEBUG: Final[bool] = get_environ_bool("TEXTUAL_DEBUG")
@@ -37,3 +55,13 @@ DRIVER: Final[str | None] = get_environ("TEXTUAL_DRIVER", None)
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
)
"""Constant with the port that the devtools will connect to."""

View File

@@ -16,8 +16,8 @@ from rich.console import Console
from rich.segment import Segment
from .._log import LogGroup, LogVerbosity
from ..constants import DEVTOOLS_PORT
DEVTOOLS_PORT = 8081
WEBSOCKET_CONNECT_TIMEOUT = 3
LOG_QUEUE_MAXSIZE = 512
@@ -88,10 +88,12 @@ class DevtoolsClient:
Args:
host: The host the devtools server is running on, defaults to "127.0.0.1"
port: The port the devtools server is accessed via, defaults to 8081
port: The port the devtools server is accessed via, `DEVTOOLS_PORT` by default.
"""
def __init__(self, host: str = "127.0.0.1", port: int = DEVTOOLS_PORT) -> None:
def __init__(self, host: str = "127.0.0.1", port: int | None = None) -> None:
if port is None:
port = DEVTOOLS_PORT
self.url: str = f"ws://{host}:{port}"
self.session: aiohttp.ClientSession | None = None
self.log_queue_task: Task | None = None

View File

@@ -46,7 +46,10 @@ def _run_devtools(verbose: bool, exclude: list[str] | None = None) -> None:
try:
run_app(
app, port=DEVTOOLS_PORT, print=noop_print, loop=asyncio.get_event_loop()
app,
port=DEVTOOLS_PORT,
print=noop_print,
loop=asyncio.get_event_loop(),
)
except OSError:
from rich import print

View File

@@ -10,6 +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.devtools.client import DevtoolsClient
from textual.devtools.redirect_output import DevtoolsLog
@@ -21,7 +22,7 @@ TIMESTAMP = 1649166819
def test_devtools_client_initialize_defaults():
devtools = DevtoolsClient()
assert devtools.url == "ws://127.0.0.1:8081"
assert devtools.url == f"ws://127.0.0.1:{DEFAULT_DEVTOOLS_PORT}"
async def test_devtools_client_is_connected(devtools):

View File

@@ -18,7 +18,7 @@ class MyApp(App[None]):
def compose(self) -> ComposeResult:
with VerticalScroll():
yield Label(("SPAM\n" * 25)[:-1])
yield Label(("SPAM\n" * 205)[:-1])
with VerticalScroll():
yield Label(("SPAM\n" * 53)[:-1])
with VerticalScroll(id="vertical"):

View File

@@ -418,6 +418,9 @@ def test_scroll_visible(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "scroll_visible.py", press=["t"])
@pytest.mark.xfail(
reason="Unpredictable while https://github.com/Textualize/textual/issues/2254 is open."
)
def test_scroll_to_center(snap_compare):
# READ THIS IF THIS TEST FAILS:
# While https://github.com/Textualize/textual/issues/2254 is open, the snapshot