Skip to content

Commit c302077

Browse files
committed
parser refactor
1 parent 62cd593 commit c302077

File tree

7 files changed

+46
-21
lines changed

7 files changed

+46
-21
lines changed

src/textual/_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def feed(self, data: str) -> Iterable[T]:
8383
if not data:
8484
self._eof = True
8585
try:
86-
self._gen.throw(EOFError())
86+
self._gen.throw(ParseEOF())
8787
except StopIteration:
8888
pass
8989
while tokens:

src/textual/_xterm_parser.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from textual import constants, events, messages
1010
from textual._ansi_sequences import ANSI_SEQUENCES_KEYS, IGNORE_SEQUENCE
1111
from textual._keyboard_protocol import FUNCTIONAL_KEYS
12-
from textual._parser import Parser, ParseTimeout, Peek1, Read1, TokenCallback
12+
from textual._parser import ParseEOF, Parser, ParseTimeout, Peek1, Read1, TokenCallback
1313
from textual.keys import KEY_NAME_REPLACEMENTS, Keys, _character_to_key
1414
from textual.message import Message
1515

@@ -187,7 +187,7 @@ def reissue_sequence_as_keys(reissue_sequence: str) -> None:
187187

188188
try:
189189
character = yield read1()
190-
except EOFError:
190+
except ParseEOF:
191191
return
192192

193193
if bracketed_paste:
@@ -216,7 +216,7 @@ def send_escape() -> None:
216216
except ParseTimeout:
217217
send_escape()
218218
break
219-
except EOFError:
219+
except ParseEOF:
220220
send_escape()
221221
return
222222

src/textual/css/parse.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
)
1818
from textual.css.styles import Styles
1919
from textual.css.tokenize import Token, tokenize, tokenize_declarations, tokenize_values
20-
from textual.css.tokenizer import EOFError, ReferencedBy
20+
from textual.css.tokenizer import ReferencedBy, UnexpectedEnd
2121
from textual.css.types import CSSLocation, Specificity3
2222
from textual.suggestions import get_suggestion
2323

@@ -66,7 +66,7 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
6666
while True:
6767
try:
6868
token = next(tokens, None)
69-
except EOFError:
69+
except UnexpectedEnd:
7070
break
7171
if token is None:
7272
break

src/textual/css/tokenizer.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ def __rich__(self) -> RenderableType:
102102
return Group(*errors)
103103

104104

105-
class EOFError(TokenError):
106-
"""Indicates that the CSS ended prematurely."""
105+
class UnexpectedEnd(TokenError):
106+
"""Indicates that the text being tokenized ended prematurely."""
107107

108108

109109
@rich.repr.auto
@@ -231,7 +231,7 @@ def get_token(self, expect: Expect) -> Token:
231231
expect: Expect object which describes which tokens may be read.
232232
233233
Raises:
234-
EOFError: If there is an unexpected end of file.
234+
UnexpectedEnd: If there is an unexpected end of file.
235235
TokenError: If there is an error with the token.
236236
237237
Returns:
@@ -251,11 +251,15 @@ def get_token(self, expect: Expect) -> Token:
251251
None,
252252
)
253253
else:
254-
raise EOFError(
254+
raise UnexpectedEnd(
255255
self.read_from,
256256
self.code,
257257
(line_no + 1, col_no + 1),
258-
"Unexpected end of file; did you forget a '}' ?",
258+
(
259+
"Unexpected end of file; did you forget a '}' ?"
260+
if expect._expect_semicolon
261+
else "Unexpected end of text"
262+
),
259263
)
260264
line = self.lines[line_no]
261265
preceding_text: str = ""
@@ -348,7 +352,7 @@ def skip_to(self, expect: Expect) -> Token:
348352
expect: Expect object describing the expected token.
349353
350354
Raises:
351-
EOFError: If end of file is reached.
355+
UnexpectedEndOfText: If end of file is reached.
352356
353357
Returns:
354358
A new token.
@@ -358,11 +362,15 @@ def skip_to(self, expect: Expect) -> Token:
358362

359363
while True:
360364
if line_no >= len(self.lines):
361-
raise EOFError(
365+
raise UnexpectedEnd(
362366
self.read_from,
363367
self.code,
364368
(line_no, col_no),
365-
"Unexpected end of file; did you forget a '}' ?",
369+
(
370+
"Unexpected end of file; did you forget a '}' ?"
371+
if expect._expect_semicolon
372+
else "Unexpected end of markup"
373+
),
366374
)
367375
line = self.lines[line_no]
368376
match = expect.search(line, col_no)

src/textual/markup.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from textual.css.parse import substitute_references
4+
from textual.css.tokenizer import UnexpectedEnd
45

56
__all__ = ["MarkupError", "escape", "to_content"]
67

@@ -40,7 +41,7 @@ class MarkupError(Exception):
4041
variable_ref=VARIABLE_REF,
4142
whitespace=r"\s+",
4243
)
43-
.expect_eof()
44+
.expect_eof(False)
4445
.expect_semicolon(False)
4546
)
4647

@@ -66,7 +67,7 @@ class MarkupError(Exception):
6667
double_string=r"\".*?\"",
6768
single_string=r"'.*?'",
6869
)
69-
.expect_eof()
70+
.expect_eof(True)
7071
.expect_semicolon(False)
7172
)
7273

@@ -302,6 +303,10 @@ def to_content(
302303
_rich_traceback_omit = True
303304
try:
304305
return _to_content(markup, style, template_variables)
306+
except UnexpectedEnd:
307+
raise MarkupError(
308+
"Unexpected end of markup; are you missing a closing square bracket?"
309+
) from None
305310
except Exception as error:
306311
# Ensure all errors are wrapped in a MarkupError
307312
raise MarkupError(str(error)) from None

tests/css/test_nested_css.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from textual.color import Color
77
from textual.containers import Vertical
88
from textual.css.parse import parse
9-
from textual.css.tokenizer import EOFError, TokenError
9+
from textual.css.tokenizer import TokenError, UnexpectedEnd
1010
from textual.widgets import Button, Label
1111

1212

@@ -94,16 +94,16 @@ async def test_rule_declaration_after_nested() -> None:
9494
@pytest.mark.parametrize(
9595
("css", "exception"),
9696
[
97-
("Selector {", EOFError),
98-
("Selector{ Foo {", EOFError),
99-
("Selector{ Foo {}", EOFError),
97+
("Selector {", UnexpectedEnd),
98+
("Selector{ Foo {", UnexpectedEnd),
99+
("Selector{ Foo {}", UnexpectedEnd),
100100
("> {}", TokenError),
101101
("&", TokenError),
102102
("&&", TokenError),
103103
("&.foo", TokenError),
104104
("& .foo", TokenError),
105105
("{", TokenError),
106-
("*{", EOFError),
106+
("*{", UnexpectedEnd),
107107
],
108108
)
109109
def test_parse_errors(css: str, exception: type[Exception]) -> None:

tests/test_content.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import pytest
34
from rich.text import Text
45

56
from textual.content import Content, Span
@@ -261,3 +262,14 @@ def test_first_line():
261262
first_line = content.first_line
262263
assert first_line.plain == "foo"
263264
assert first_line.spans == [Span(0, 3, "red")]
265+
266+
267+
def test_errors():
268+
with pytest.raises(Exception):
269+
Content.from_markup("[")
270+
271+
with pytest.raises(Exception):
272+
Content.from_markup("[:")
273+
274+
with pytest.raises(Exception):
275+
Content.from_markup("[foo")

0 commit comments

Comments
 (0)