error handling

This commit is contained in:
Will McGugan
2021-11-12 15:26:27 +00:00
parent c669ab85d4
commit 9856f2a1c3
11 changed files with 162 additions and 65 deletions

View File

@@ -3,21 +3,27 @@ from textual.widgets import Placeholder
class BasicApp(App):
"""Demonstrates smooth animation. Press 'b' to see it in action."""
"""A basic app demonstrating CSS"""
css = """
App > View {
layout: dock
layout: dock;
docks: sidebar=left | widgets=top;
}
#sidebar {
dock-group: sidebar;
}
#widget1 {
edge: top
text: on blue;
dock-group: widgets;
}
#widget2 {
text: on red;
dock-group: widgets;
}
"""
@@ -25,7 +31,9 @@ class BasicApp(App):
async def on_mount(self) -> None:
"""Build layout here."""
await self.view.mount(widget1=Placeholder(), widget2=Placeholder())
await self.view.mount(
sidebar=Placeholder(), widget1=Placeholder(), widget2=Placeholder()
)
BasicApp.run(log="textual.log")

View File

@@ -23,9 +23,8 @@ class StylesBuilder:
def __rich_repr__(self) -> rich.repr.Result:
yield "styles", self.styles
def error(self, name: str, token: Token, msg: str) -> None:
line, col = token.location
raise DeclarationError(name, token, f"{msg} (line {line + 1}, col {col + 1})")
def error(self, name: str, token: Token, message: str) -> None:
raise DeclarationError(name, token, message)
def add_declaration(self, declaration: Declaration) -> None:
if not declaration.tokens:
@@ -36,7 +35,7 @@ class StylesBuilder:
if process_method is None:
self.error(
declaration.name,
declaration.tokens[0],
declaration.token,
f"unknown declaration {declaration.name!r}",
)
else:
@@ -45,15 +44,20 @@ class StylesBuilder:
tokens = tokens[:-1]
self.styles.important.add(declaration.name)
if process_method is not None:
process_method(declaration.name, tokens)
try:
process_method(declaration.name, tokens)
except DeclarationError as error:
self.error(error.name, error.token, error.message)
except Exception as error:
self.error(declaration.name, declaration.token, str(error))
def process_display(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
location, name, value = token
_, _, location, name, value = token
if name == "token":
value = value.lower()
if value in VALID_DISPLAY:
self.styles._display = cast(Display, value)
self.styles._rule_display = cast(Display, value)
else:
self.error(
name,
@@ -65,11 +69,11 @@ class StylesBuilder:
def process_visibility(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
location, name, value = token
_, _, location, name, value = token
if name == "token":
value = value.lower()
if value in VALID_VISIBILITY:
self.styles._visibility = cast(Visibility, value)
self.styles._rule_visibility = cast(Visibility, value)
else:
self.error(
name,
@@ -83,7 +87,7 @@ class StylesBuilder:
space: list[int] = []
append = space.append
for token in tokens:
location, toke_name, value = token
_, _, location, toke_name, value = token
if toke_name == "number":
append(int(value))
else:
@@ -110,7 +114,7 @@ class StylesBuilder:
style_tokens: list[str] = []
append = style_tokens.append
for token in tokens:
location, token_name, value = token
_, _, location, token_name, value = token
if token_name == "token":
if value in VALID_BORDER:
border_type = value
@@ -206,6 +210,13 @@ class StylesBuilder:
x = self.styles.offset.x
self.styles._rule_offset = Offset(x, y)
def process_layout(self, name: str, tokens: list[Token]) -> None:
if tokens:
if len(tokens) != 1:
self.error(name, tokens[0], "unexpected tokens in declaration")
else:
self.styles._rule_layout = tokens[0].value
def process_text(self, name: str, tokens: list[Token]) -> None:
style_definition = " ".join(token.value for token in tokens)
style = Style.parse(style_definition)
@@ -259,6 +270,7 @@ class StylesBuilder:
if token.name == "token":
docks.append(token.value)
else:
self.error(
name,
token,

View File

@@ -3,7 +3,9 @@ from .tokenize import Token
class DeclarationError(Exception):
def __init__(self, name: str, token: Token, message: str) -> None:
self.name = name
self.token = token
self.message = message
super().__init__(message)

View File

@@ -97,6 +97,7 @@ class Selector:
@dataclass
class Declaration:
token: Token
name: str
tokens: list[Token] = field(default_factory=list)
@@ -134,6 +135,7 @@ class SelectorSet:
class RuleSet:
selector_set: list[SelectorSet] = field(default_factory=list)
styles: Styles = field(default_factory=Styles)
errors: list[tuple[Token, str]] = field(default_factory=list)
@classmethod
def _selector_to_css(cls, selectors: list[Selector]) -> str:

View File

@@ -16,7 +16,7 @@ from .model import (
SelectorSet,
SelectorType,
)
from ._styles_builder import StylesBuilder
from ._styles_builder import StylesBuilder, DeclarationError
SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
@@ -34,7 +34,7 @@ SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
@lru_cache(maxsize=1024)
def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
tokens = iter(tokenize(css_selectors))
tokens = iter(tokenize(css_selectors, ""))
get_selector = SELECTOR_MAP.get
combinator: CombinatorType | None = CombinatorType.DESCENDENT
@@ -122,7 +122,9 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
if selectors:
rule_selectors.append(selectors[:])
declaration = Declaration("")
declaration = Declaration(token, "")
errors: list[tuple[Token, str]] = []
while True:
token = next(tokens)
@@ -131,8 +133,11 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
continue
if token_name == "declaration_name":
if declaration.tokens:
styles_builder.add_declaration(declaration)
declaration = Declaration("")
try:
styles_builder.add_declaration(declaration)
except DeclarationError as error:
errors.append((error.token, error.message))
declaration = Declaration(token, "")
declaration.name = token.value.rstrip(":")
elif token_name == "declaration_set_end":
break
@@ -140,17 +145,20 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
declaration.tokens.append(token)
if declaration.tokens:
styles_builder.add_declaration(declaration)
try:
styles_builder.add_declaration(declaration)
except DeclarationError as error:
errors.append((error.token, error.message))
rule_set = RuleSet(
list(SelectorSet.from_selectors(rule_selectors)), styles_builder.styles
list(SelectorSet.from_selectors(rule_selectors)), styles_builder.styles, errors
)
yield rule_set
def parse(css: str) -> Iterable[RuleSet]:
def parse(css: str, path: str) -> Iterable[RuleSet]:
tokens = iter(tokenize(css))
tokens = iter(tokenize(css, path))
while True:
token = next(tokens, None)
if token is None:

View File

@@ -65,6 +65,8 @@ class Styles:
_rule_fraction: int | None = None
_rule_min_size: int | None = None
_rule_layout: str | None = None
_rule_dock_group: str | None = None
_rule_dock_edge: str | None = None
_rule_docks: tuple[str, ...] | None = None

View File

@@ -2,9 +2,16 @@ from __future__ import annotations
from collections import defaultdict
from operator import itemgetter
import os
from typing import Iterable, TYPE_CHECKING
import rich.repr
from rich.highlighter import ReprHighlighter
from rich.panel import Panel
from rich.syntax import Syntax
from rich.table import Table
from rich.text import TextType, Text
from rich.console import Group, RenderableType
from .errors import StylesheetError
@@ -15,6 +22,43 @@ from .types import Specificity3, Specificity4
from ..dom import DOMNode
class StylesheetErrors:
def __init__(self, stylesheet: "Stylesheet") -> None:
self.stylesheet = stylesheet
@classmethod
def _get_snippet(cls, code: str, line_no: int, col_no: int, length: int) -> Text:
lines = Text(code, style="dim").split()
lines[line_no].stylize("bold not dim", col_no, col_no + length - 1)
text = Text("\n").join(lines[max(0, line_no - 1) : line_no + 2])
# return Syntax(
# code,
# "",
# line_range=(line_no - 1, line_no + 1),
# line_numbers=True,
# indent_guides=True,
# )
return Panel(text, border_style="red")
def __rich__(self) -> RenderableType:
highlighter = ReprHighlighter()
errors: list[RenderableType] = []
append = errors.append
for rule in self.stylesheet.rules:
for token, message in rule.errors:
line_no, col_no = token.location
append(highlighter(f"{token.path}:{line_no}"))
append(
self._get_snippet(token.code, line_no, col_no, len(token.value) + 1)
)
append(highlighter(Text(message, "red")))
append("")
return Group(*errors)
error_text = Text("\n").join(errors)
return error_text
@rich.repr.auto
class Stylesheet:
def __init__(self) -> None:
@@ -27,21 +71,32 @@ class Stylesheet:
def css(self) -> str:
return "\n\n".join(rule_set.css for rule_set in self.rules)
@property
def any_errors(self) -> bool:
"""Check if there are any errors."""
return any(rule.errors for rule in self.rules)
@property
def error_renderable(self) -> StylesheetErrors:
return StylesheetErrors(self)
def read(self, filename: str) -> None:
filename = os.path.expanduser(filename)
try:
with open(filename, "rt") as css_file:
css = css_file.read()
path = os.path.abspath(filename)
except Exception as error:
raise StylesheetError(f"unable to read {filename!r}; {error}") from None
try:
rules = list(parse(css))
rules = list(parse(css, path))
except Exception as error:
raise StylesheetError(f"failed to parse {filename!r}; {error}") from None
self.rules.extend(rules)
def parse(self, css: str) -> None:
def parse(self, css: str, path: str = "") -> None:
try:
rules = list(parse(css))
rules = list(parse(css, path))
except Exception as error:
raise StylesheetError(f"failed to parse css; {error}")
self.rules.extend(rules)
@@ -119,45 +174,41 @@ if __name__ == "__main__":
from rich import print
print(app.tree)
print()
CSS = """
App > View {
text: red;
layout: dock;
docks: sidebar=left | widgets=top;
fart: poo
}
* {
text: blue
#sidebar {
dock-group: sidebar;
}
Widget{
text: red;
text-style: bold
#widget1 {
text: on blue;
dock-group: widgets;
}
/*
View #widget1 {
text: green !important;
#widget2 {
text: on red;
dock-group: widgets;
}
*/
App > View.-subview {
outline: heavy
}
"""
stylesheet = Stylesheet()
stylesheet.parse(CSS)
stylesheet.read("~/example.css")
print(widget1.styles)
print(stylesheet.error_renderable)
stylesheet.apply(widget1)
# print(widget1.styles)
print(widget1.styles)
# stylesheet.apply(widget1)
# print(widget1.styles)
# from .query import DOMQuery

View File

@@ -47,8 +47,10 @@ expect_declaration_content = Expect(
percentage=r"\d+\%",
number=r"\d+\.?\d*",
color=r"\#[0-9a-f]{6}|color\([0-9]{1,3}\)|rgb\(\d{1,3}\,\s?\d{1,3}\,\s?\d{1,3}\)",
key_value=r"[a-zA-Z_-][a-zA-Z0-9_-]*=[a-zA-Z_-]+",
token="[a-zA-Z_-]+",
string=r"\".*?\"",
bar=r"\|",
important=r"\!important",
declaration_set_end=r"\}",
)
@@ -69,8 +71,8 @@ _STATES = {
}
def tokenize(code: str) -> Iterable[Token]:
tokenizer = Tokenizer(code)
def tokenize(code: str, path: str) -> Iterable[Token]:
tokenizer = Tokenizer(code, path=path)
expect = expect_selector
get_token = tokenizer.get_token
get_state = _STATES.get

View File

@@ -40,6 +40,8 @@ class Expect:
class Token(NamedTuple):
path: str
code: str
location: tuple[int, int]
name: str
value: str
@@ -49,7 +51,9 @@ class Token(NamedTuple):
class Tokenizer:
def __init__(self, text: str) -> None:
def __init__(self, text: str, path: str = "") -> None:
self.path = path
self.code = text
self.lines = text.splitlines(keepends=True)
self.line_no = 0
self.col_no = 0
@@ -59,7 +63,7 @@ class Tokenizer:
col_no = self.col_no
if line_no >= len(self.lines):
if expect._expect_eof:
return Token((line_no, col_no), "eof", "")
return Token(self.path, self.code, (line_no, col_no), "eof", "")
else:
raise EOFError()
line = self.lines[line_no]
@@ -77,7 +81,7 @@ class Tokenizer:
if value is not None:
break
token = Token((line_no, col_no), name, value)
token = Token(self.path, self.code, (line_no, col_no), name, value)
col_no += len(value)
if col_no >= len(line):
line_no += 1

View File

@@ -42,6 +42,15 @@ class DOMNode(MessagePump):
def id(self) -> str | None:
return self._id
@id.setter
def id(self, new_id: str) -> str:
if self._id is not None:
raise ValueError(
"Node 'id' attribute may not be changed once set (current id={self._id!r})"
)
self._id = new_id
return new_id
@property
def name(self) -> str | None:
return self._name

View File

@@ -140,16 +140,13 @@ class View(Widget):
async def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
name_widgets: Iterable[tuple[str | None, Widget]]
name_widgets = chain(
((None, widget) for widget in anon_widgets), widgets.items()
)
stylesheet = self.app.stylesheet
for name, widget in name_widgets:
if name is not None:
widget.name = name
stylesheet.apply(widget)
name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()]
apply_stylesheet = self.app.stylesheet.apply
for widget_id, widget in name_widgets:
if widget_id is not None:
widget.id = widget_id
apply_stylesheet(widget)
self._add_child(widget)
self.refresh()
async def refresh_layout(self) -> None: