Skip to content

Commit

Permalink
check errors
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Jan 4, 2024
1 parent 711d9ec commit 29928a3
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 8 deletions.
2 changes: 1 addition & 1 deletion src/textual/css/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def _add_specificity(
specificity2: Specificity triple.
Returns:
Combined specificity
Combined specificity.
"""
a1, b1, c1 = specificity1
a2, b2, c2 = specificity2
Expand Down
2 changes: 1 addition & 1 deletion src/textual/css/stylesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def _parse_rules(
except TokenError:
raise
except Exception as error:
raise StylesheetError(f"failed to parse css; {error}")
raise StylesheetError(f"failed to parse css; {error}") from None

self._parse_cache[cache_key] = rules
return rules
Expand Down
25 changes: 23 additions & 2 deletions src/textual/css/tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,22 @@
selector_start=IDENTIFIER,
variable_name=rf"{VARIABLE_REF}:",
declaration_set_end=r"\}",
nested=r"\&",
).expect_eof(True)

expect_root_nested = Expect(
"selector or end of file",
whitespace=r"\s+",
comment_start=COMMENT_START,
comment_line=COMMENT_LINE,
selector_start_id=r"\#" + IDENTIFIER,
selector_start_class=r"\." + IDENTIFIER,
selector_start_universal=r"\*",
selector_start=IDENTIFIER,
variable_name=rf"{VARIABLE_REF}:",
declaration_set_end=r"\}",
nested=r"\&",
)

# After a variable declaration e.g. "$warning-text: TOKENS;"
# for tokenizing variable value ------^~~~~~~^
expect_variable_name_continue = Expect(
Expand Down Expand Up @@ -173,7 +186,7 @@ class TokenizerState:
"declaration_set_start": expect_declaration,
"declaration_name": expect_declaration_content,
"declaration_end": expect_declaration,
"declaration_set_end": expect_root_scope,
"declaration_set_end": expect_root_nested,
"nested": expect_selector_continue,
}

Expand All @@ -182,6 +195,7 @@ def __call__(self, code: str, read_from: CSSLocation) -> Iterable[Token]:
expect = self.EXPECT
get_token = tokenizer.get_token
get_state = self.STATE_MAP.get
nest_level = 0
while True:
token = get_token(expect)
name = token.name
Expand All @@ -192,6 +206,13 @@ def __call__(self, code: str, read_from: CSSLocation) -> Iterable[Token]:
continue
elif name == "eof":
break
elif name == "declaration_set_start":
nest_level += 1
elif name == "declaration_set_end":
nest_level -= 1
expect = expect_root_nested if nest_level else expect_root_scope
yield token
continue
expect = get_state(name, expect)
yield token

Expand Down
5 changes: 2 additions & 3 deletions src/textual/css/tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,13 +215,12 @@ def get_token(self, expect: Expect) -> Token:
self.read_from,
self.code,
(line_no + 1, col_no + 1),
"Unexpected end of file",
"Unexpected end of file; did you forget a '}' ?",
)
line = self.lines[line_no]
match = expect.match(line, col_no)
if match is None:
expected = friendly_list(" ".join(name.split("_")) for name in expect.names)
message = f"Expected one of {expected}.; Did you forget a semicolon at the end of a line?"
raise TokenError(
self.read_from,
self.code,
Expand Down Expand Up @@ -288,7 +287,7 @@ def skip_to(self, expect: Expect) -> Token:
self.read_from,
self.code,
(line_no, col_no),
"Unexpected end of file",
"Unexpected end of file; did you forget a '}' ?",
)
line = self.lines[line_no]
match = expect.search(line, col_no)
Expand Down
24 changes: 23 additions & 1 deletion tests/css/test_nested_css.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import pytest

from textual.app import App, ComposeResult
from textual.color import Color
from textual.containers import Vertical
from textual.css.parse import parse
from textual.css.tokenizer import EOFError, TokenError
from textual.widgets import Label


class NestedApp(App):
CSS = """
Screen {
#foo {
& > #foo {
background: red;
#egg {
background: green;
Expand Down Expand Up @@ -36,3 +40,21 @@ async def test_nest_app():
assert app.query_one("#foo").styles.color == Color.parse("magenta")
assert app.query_one("#egg").styles.background == Color.parse("green")
assert app.query_one("#foo .paul").styles.background == Color.parse("blue")


@pytest.mark.parametrize(
("css", "exception"),
[
("Selector {", EOFError),
("Selector{ Foo {", EOFError),
("Selector{ Foo {}", EOFError),
("> {}", TokenError),
("&", TokenError),
("&.foo", TokenError),
("{", TokenError),
],
)
def test_parse_errors(css: str, exception: type[Exception]) -> None:
"""Check some CSS which should fail."""
with pytest.raises(exception):
list(parse("", css, ("foo", "")))

0 comments on commit 29928a3

Please sign in to comment.