diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2529ba84..8e43b090c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/guide/devtools.md b/docs/guide/devtools.md index dd0f168b8e..4fbffe6cbf 100644 --- a/docs/guide/devtools.md +++ b/docs/guide/devtools.md @@ -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 diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index c4d1da0a19..c1186f99d7 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -2,6 +2,8 @@ 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") diff --git a/src/textual/constants.py b/src/textual/constants.py index f3762b33d6..c73fe31af1 100644 --- a/src/textual/constants.py +++ b/src/textual/constants.py @@ -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 @@ def get_environ_bool(name: str) -> bool: 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.""" diff --git a/src/textual/devtools/client.py b/src/textual/devtools/client.py index 3c8592ae61..2982d07feb 100644 --- a/src/textual/devtools/client.py +++ b/src/textual/devtools/client.py @@ -16,8 +16,8 @@ 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 diff --git a/src/textual/devtools/server.py b/src/textual/devtools/server.py index 4228ca6f53..4e7f66814b 100644 --- a/src/textual/devtools/server.py +++ b/src/textual/devtools/server.py @@ -46,7 +46,10 @@ def noop_print(_: str) -> 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 diff --git a/tests/devtools/test_devtools_client.py b/tests/devtools/test_devtools_client.py index 88088bff06..b67f558ba4 100644 --- a/tests/devtools/test_devtools_client.py +++ b/tests/devtools/test_devtools_client.py @@ -10,6 +10,7 @@ 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 @@ 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): diff --git a/tests/snapshot_tests/snapshot_apps/scroll_to_center.py b/tests/snapshot_tests/snapshot_apps/scroll_to_center.py index 269775fed3..d26ee7a84f 100644 --- a/tests/snapshot_tests/snapshot_apps/scroll_to_center.py +++ b/tests/snapshot_tests/snapshot_apps/scroll_to_center.py @@ -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"): diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 35a0cebc2c..e8bde50c72 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -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