Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions docs/guide/devtools.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 30 additions & 3 deletions src/textual/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import sys

from ..constants import DEFAULT_DEVTOOLS_PORT, DEVTOOLS_PORT_ENVIRON_VARIABLE

try:
import click
except ImportError:
Expand All @@ -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)
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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")
Expand Down
30 changes: 29 additions & 1 deletion src/textual/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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."""
8 changes: 5 additions & 3 deletions src/textual/devtools/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/textual/devtools/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/devtools/test_devtools_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/snapshot_tests/snapshot_apps/scroll_to_center.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
3 changes: 3 additions & 0 deletions tests/snapshot_tests/test_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down