mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
error handling
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user