Skip to content

Commit

Permalink
Merge branch 'main' into force-tree-node-scroll
Browse files Browse the repository at this point in the history
  • Loading branch information
davep committed Nov 30, 2023
2 parents 83583d4 + be8581e commit 817af47
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- `link-hover-color` renamed to `link-color-hover`
- `link-hover-style` renamed to `link-style-hover`
- `Tree` now forces a scroll when `scroll_to_node` is called https://github.com/Textualize/textual/pull/3786
- Brought rxvt's use of shift-numpad keys in line with most other terminals https://github.com/Textualize/textual/pull/3769

### Added

- Added support for Ctrl+Fn and Ctrl+Shift+Fn keys in urxvt https://github.com/Textualize/textual/pull/3737
- Friendly error messages when trying to mount non-widgets https://github.com/Textualize/textual/pull/3780

## [0.43.2] - 2023-11-29

Expand Down
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ You can follow these steps:
1. Make sure you have Poetry installed ([see instructions here](https://python-poetry.org))
2. Clone the Textual repository
3. Run `poetry shell` to create a virtual environment for the dependencies
4. Run `poetry install` to install all dependencies
4. Run `make setup` to install all dependencies
5. Make sure the latest version of Textual was installed by running the command `textual --version`
6. Install the pre-commit hooks with the command `pre-commit install`

([Read this](#makefile-commands) if the command `make` doesn't work for you.)

## Demo

Once you have Textual installed, run the Textual demo to get an impression of what Textual can do and to double check that everything was installed correctly:
Expand Down
22 changes: 21 additions & 1 deletion src/textual/_ansi_sequences.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

from typing import Mapping, Tuple

from .keys import Keys

# Mapping of vt100 escape codes to Keys.
ANSI_SEQUENCES_KEYS: Mapping[str, Tuple[Keys, ...]] = {
ANSI_SEQUENCES_KEYS: Mapping[str, Tuple[Keys, ...] | str] = {
# Control keys.
" ": (Keys.Space,),
"\r": (Keys.Enter,),
Expand Down Expand Up @@ -350,6 +352,24 @@
"\x1b[1;8w": (Keys.Escape, Keys.ControlShift7),
"\x1b[1;8x": (Keys.Escape, Keys.ControlShift8),
"\x1b[1;8y": (Keys.Escape, Keys.ControlShift9),
# Simplify some sequences that appear to be unique to rxvt; see
# https://github.com/Textualize/textual/issues/3741 for context.
"\x1bOj": "*",
"\x1bOk": "+",
"\x1bOm": "-",
"\x1bOn": ".",
"\x1bOo": "/",
"\x1bOp": "0",
"\x1bOq": "1",
"\x1bOr": "2",
"\x1bOs": "3",
"\x1bOt": "4",
"\x1bOu": "5",
"\x1bOv": "6",
"\x1bOw": "7",
"\x1bOx": "8",
"\x1bOy": "9",
"\x1bOM": (Keys.Enter,),
}

# https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
Expand Down
26 changes: 25 additions & 1 deletion src/textual/_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,51 @@ def compose(node: App | Widget) -> list[Widget]:
A list of widgets.
"""
_rich_traceback_omit = True
from .widget import MountError, Widget

app = node.app
nodes: list[Widget] = []
compose_stack: list[Widget] = []
composed: list[Widget] = []
app._compose_stacks.append(compose_stack)
app._composed.append(composed)
iter_compose = iter(node.compose())
is_generator = hasattr(iter_compose, "throw")
try:
while True:
try:
child = next(iter_compose)
except StopIteration:
break

if not isinstance(child, Widget):
mount_error = MountError(
f"Can't mount {type(child)}; expected a Widget instance."
)
if is_generator:
iter_compose.throw(mount_error) # type: ignore
else:
raise mount_error from None

try:
child.id
except AttributeError:
mount_error = MountError(
"Widget is missing an 'id' attribute; did you forget to call super().__init__()?"
)
if is_generator:
iter_compose.throw(mount_error) # type: ignore
else:
raise mount_error from None

if composed:
nodes.extend(composed)
composed.clear()
if compose_stack:
try:
compose_stack[-1].compose_add_child(child)
except Exception as error:
if hasattr(iter_compose, "throw"):
if is_generator:
# So the error is raised inside the generator
# This will generate a more sensible traceback for the dev
iter_compose.throw(error) # type: ignore
Expand Down
14 changes: 12 additions & 2 deletions src/textual/_xterm_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,20 @@ def _sequence_to_key_events(
Keys
"""
keys = ANSI_SEQUENCES_KEYS.get(sequence)
if keys is not None:
if isinstance(keys, tuple):
# If the sequence mapped to a tuple, then it's values from the
# `Keys` enum. Raise key events from what we find in the tuple.
for key in keys:
yield events.Key(key.value, sequence if len(sequence) == 1 else None)
elif len(sequence) == 1:
return
# If keys is a string, the intention is that it's a mapping to a
# character, which should really be treated as the sequence for the
# purposes of the next step...
if isinstance(keys, str):
sequence = keys
# If the sequence is a single character, attempt to process it as a
# key.
if len(sequence) == 1:
try:
if not sequence.isalnum():
name = _character_to_key(sequence)
Expand Down
1 change: 1 addition & 0 deletions src/textual/pilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ async def _post_mouse_events(
# the driver works and emits a click event.
widget_at, _ = app.get_widget_at(*offset)
event = mouse_event_cls(**message_arguments)
# Bypass event processing in App.on_event
app.screen._forward_event(event)
await self.pause()

Expand Down
1 change: 0 additions & 1 deletion src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,6 @@ def mount(
Only one of ``before`` or ``after`` can be provided. If both are
provided a ``MountError`` will be raised.
"""

# Check for duplicate IDs in the incoming widgets
ids_to_mount = [widget.id for widget in widgets if widget.id is not None]
unique_ids = set(ids_to_mount)
Expand Down
27 changes: 27 additions & 0 deletions tests/test_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,30 @@ def compose(self) -> ComposeResult:
with pytest.raises(NotAContainer):
async with app.run_test():
pass


async def test_mount_error_not_widget():
class NotWidgetApp(App):
def compose(self) -> ComposeResult:
yield {}

app = NotWidgetApp()
with pytest.raises(MountError):
async with app.run_test():
pass


async def test_mount_error_bad_widget():
class DaftWidget(Widget):
def __init__(self):
# intentionally missing super()
pass

class NotWidgetApp(App):
def compose(self) -> ComposeResult:
yield DaftWidget()

app = NotWidgetApp()
with pytest.raises(MountError):
async with app.run_test():
pass

0 comments on commit 817af47

Please sign in to comment.