Skip to content

Commit b2af20c

Browse files
authored
Merge pull request #4848 from Textualize/esc-delay
add ESCDELAY environment var
2 parents 3bdd363 + 1a400cc commit b2af20c

File tree

11 files changed

+203
-296
lines changed

11 files changed

+203
-296
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1919

2020
- Input cursor blink effect will now restart correctly when any action is performed on the input https://github.com/Textualize/textual/pull/4773
2121

22+
### Added
23+
24+
- Textual will use the `ESCDELAY` env var when detecting escape keys https://github.com/Textualize/textual/pull/4848
25+
2226
## [0.75.1] - 2024-08-02
2327

2428
### Fixed

src/textual/_parser.py

Lines changed: 66 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,126 @@
11
from __future__ import annotations
22

3-
import io
43
from collections import deque
5-
from typing import Callable, Deque, Generator, Generic, Iterable, TypeVar, Union
4+
from typing import Callable, Deque, Generator, Generic, Iterable, NamedTuple, TypeVar
5+
6+
from ._time import get_time
67

78

89
class ParseError(Exception):
9-
pass
10+
"""Base class for parse related errors."""
1011

1112

1213
class ParseEOF(ParseError):
1314
"""End of Stream."""
1415

1516

16-
class Awaitable:
17-
__slots__: list[str] = []
18-
19-
20-
class _Read(Awaitable):
21-
__slots__ = ["remaining"]
22-
23-
def __init__(self, count: int) -> None:
24-
self.remaining = count
17+
class ParseTimeout(ParseError):
18+
"""Read has timed out."""
2519

26-
def __repr__(self) -> str:
27-
return f"_ReadBytes({self.remaining})"
2820

21+
class Read1(NamedTuple):
22+
"""Reads a single character."""
2923

30-
class _Read1(Awaitable):
31-
__slots__: list[str] = []
24+
timeout: float | None = None
25+
"""Optional timeout in seconds."""
3226

3327

34-
class _ReadUntil(Awaitable):
35-
__slots__ = ["sep", "max_bytes"]
28+
class Peek1(NamedTuple):
29+
"""Reads a single character, but does not advance the parser position."""
3630

37-
def __init__(self, sep: str, max_bytes: int | None = None) -> None:
38-
self.sep = sep
39-
self.max_bytes = max_bytes
40-
41-
42-
class _PeekBuffer(Awaitable):
43-
__slots__: list[str] = []
31+
timeout: float | None = None
32+
"""Optional timeout in seconds."""
4433

4534

4635
T = TypeVar("T")
47-
48-
4936
TokenCallback = Callable[[T], None]
5037

5138

5239
class Parser(Generic[T]):
53-
read = _Read
54-
read1 = _Read1
55-
read_until = _ReadUntil
56-
peek_buffer = _PeekBuffer
40+
"""Base class for a simple parser."""
41+
42+
read1 = Read1
43+
peek1 = Peek1
5744

5845
def __init__(self) -> None:
59-
self._buffer = io.StringIO()
6046
self._eof = False
6147
self._tokens: Deque[T] = deque()
6248
self._gen = self.parse(self._tokens.append)
63-
self._awaiting: Union[Awaitable, T] = next(self._gen)
49+
self._awaiting: Read1 | Peek1 = next(self._gen)
50+
self._timeout_time: float | None = None
6451

6552
@property
6653
def is_eof(self) -> bool:
54+
"""Is the parser at the end of the file (i.e. exhausted)?"""
6755
return self._eof
6856

69-
def reset(self) -> None:
70-
self._gen = self.parse(self._tokens.append)
71-
self._awaiting = next(self._gen)
57+
def tick(self) -> Iterable[T]:
58+
"""Call at regular intervals to check for timeouts."""
59+
if self._timeout_time is not None and get_time() >= self._timeout_time:
60+
self._timeout_time = None
61+
self._awaiting = self._gen.throw(ParseTimeout())
62+
while self._tokens:
63+
yield self._tokens.popleft()
7264

7365
def feed(self, data: str) -> Iterable[T]:
66+
"""Feed data to be parsed.
67+
68+
Args:
69+
data: Data to parser.
70+
71+
Raises:
72+
ParseError: If the data could not be parsed.
73+
74+
Yields:
75+
T: A generic data type.
76+
"""
7477
if self._eof:
7578
raise ParseError("end of file reached") from None
79+
80+
tokens = self._tokens
81+
popleft = tokens.popleft
82+
7683
if not data:
7784
self._eof = True
7885
try:
79-
self._gen.send(self._buffer.getvalue())
86+
self._gen.throw(EOFError())
8087
except StopIteration:
81-
raise ParseError("end of file reached") from None
82-
while self._tokens:
83-
yield self._tokens.popleft()
84-
85-
self._buffer.truncate(0)
88+
pass
89+
while tokens:
90+
yield popleft()
8691
return
8792

88-
_buffer = self._buffer
8993
pos = 0
90-
tokens = self._tokens
91-
popleft = tokens.popleft
9294
data_size = len(data)
9395

9496
while tokens:
9597
yield popleft()
9698

97-
while pos < data_size or isinstance(self._awaiting, _PeekBuffer):
99+
while pos < data_size:
98100
_awaiting = self._awaiting
99-
if isinstance(_awaiting, _Read1):
100-
self._awaiting = self._gen.send(data[pos : pos + 1])
101+
if isinstance(_awaiting, Read1):
102+
self._timeout_time = None
103+
self._awaiting = self._gen.send(data[pos])
101104
pos += 1
105+
elif isinstance(_awaiting, Peek1):
106+
self._timeout_time = None
107+
self._awaiting = self._gen.send(data[pos])
102108

103-
elif isinstance(_awaiting, _PeekBuffer):
104-
self._awaiting = self._gen.send(data[pos:])
105-
106-
elif isinstance(_awaiting, _Read):
107-
remaining = _awaiting.remaining
108-
chunk = data[pos : pos + remaining]
109-
chunk_size = len(chunk)
110-
pos += chunk_size
111-
_buffer.write(chunk)
112-
remaining -= chunk_size
113-
if remaining:
114-
_awaiting.remaining = remaining
115-
else:
116-
_awaiting = self._gen.send(_buffer.getvalue())
117-
_buffer.seek(0)
118-
_buffer.truncate()
119-
120-
elif isinstance(_awaiting, _ReadUntil):
121-
chunk = data[pos:]
122-
_buffer.write(chunk)
123-
sep = _awaiting.sep
124-
sep_index = _buffer.getvalue().find(sep)
125-
126-
if sep_index == -1:
127-
pos += len(chunk)
128-
if (
129-
_awaiting.max_bytes is not None
130-
and _buffer.tell() > _awaiting.max_bytes
131-
):
132-
self._gen.throw(ParseError(f"expected {sep}"))
133-
else:
134-
sep_index += len(sep)
135-
if (
136-
_awaiting.max_bytes is not None
137-
and sep_index > _awaiting.max_bytes
138-
):
139-
self._gen.throw(ParseError(f"expected {sep}"))
140-
data = _buffer.getvalue()[sep_index:]
141-
pos = 0
142-
self._awaiting = self._gen.send(_buffer.getvalue()[:sep_index])
143-
_buffer.seek(0)
144-
_buffer.truncate()
109+
if self._awaiting.timeout is not None:
110+
self._timeout_time = get_time() + self._awaiting.timeout
145111

146112
while tokens:
147113
yield popleft()
148114

149-
def parse(self, on_token: Callable[[T], None]) -> Generator[Awaitable, str, None]:
150-
yield from ()
151-
115+
def parse(
116+
self, token_callback: TokenCallback
117+
) -> Generator[Read1 | Peek1, str, None]:
118+
"""Implement to parse a stream of text.
152119
153-
if __name__ == "__main__":
154-
data = "Where there is a Will there is a way!"
120+
Args:
121+
token_callback: Callable to report a successful parsed data type.
155122
156-
class TestParser(Parser[str]):
157-
def parse(
158-
self, on_token: Callable[[str], None]
159-
) -> Generator[Awaitable, str, None]:
160-
while True:
161-
data = yield self.read1()
162-
if not data:
163-
break
164-
on_token(data)
165-
166-
test_parser = TestParser()
167-
168-
for n in range(0, len(data), 5):
169-
for token in test_parser.feed(data[n : n + 5]):
170-
print(token)
171-
for token in test_parser.feed(""):
172-
print(token)
123+
Yields:
124+
ParseAwaitable: One of `self.read1` or `self.peek1`
125+
"""
126+
yield from ()

0 commit comments

Comments
 (0)