fix css reload

This commit is contained in:
Will McGugan
2022-04-29 10:20:32 +01:00
parent ee82f28407
commit 233c2c4075
9 changed files with 87 additions and 70 deletions

View File

@@ -1,7 +1,7 @@
#uber1 {
layout: vertical;
background: dark_green;
background: green;
overflow: hidden auto;
border: heavy white;
}

View File

@@ -37,7 +37,7 @@ class BasicApp(App):
await self.dispatch_key(event)
def action_quit(self):
self.panic(self.screen.tree)
self.panic(self.app.tree)
def action_dump(self):
self.panic(str(self.app.registry))

View File

@@ -330,10 +330,10 @@ class App(Generic[ReturnType], DOMNode):
async def _on_css_change(self) -> None:
"""Called when the CSS changes (if watch_css is True)."""
if self.css_file is not None:
stylesheet = Stylesheet(variables=self.get_css_variables())
try:
time = perf_counter()
stylesheet.read(self.css_file)
self.stylesheet.read(self.css_file)
elapsed = (perf_counter() - time) * 1000
self.log(f"loaded {self.css_file} in {elapsed:.0f}ms")
except Exception as error:
@@ -342,7 +342,6 @@ class App(Generic[ReturnType], DOMNode):
self.log(error)
else:
self.reset_styles()
self.stylesheet = stylesheet
self.stylesheet.update(self)
self.screen.refresh(layout=True)
@@ -506,7 +505,9 @@ class App(Generic[ReturnType], DOMNode):
if self.css_file is not None:
self.stylesheet.read(self.css_file)
if self.css is not None:
self.stylesheet.parse(self.css, path=f"<{self.__class__.__name__}>")
self.stylesheet.add_source(
self.css, path=f"<{self.__class__.__name__}>"
)
except Exception as error:
self.on_exception(error)
self._print_error_renderables()

View File

@@ -340,7 +340,7 @@ if __name__ == "__main__":
console = Console()
stylesheet = Stylesheet()
try:
stylesheet.parse(css)
stylesheet.add_source(css)
except StylesheetParseError as e:
console.print(e.errors)
print(stylesheet)

View File

@@ -101,21 +101,23 @@ class StylesheetErrors:
@rich.repr.auto
class Stylesheet:
def __init__(self, *, variables: dict[str, str] | None = None) -> None:
self.rules: list[RuleSet] = []
self._rules: list[RuleSet] | None = None
self.variables = variables or {}
self.source: list[tuple[str, str]] = []
self.source: dict[str, str] = {}
def __rich_repr__(self) -> rich.repr.Result:
yield self.rules
@property
def css(self) -> str:
return "\n\n".join(rule_set.css for rule_set in self.rules)
def rules(self) -> list[RuleSet]:
if self._rules is None:
self.parse()
assert self._rules is not None
return self._rules
@property
def any_errors(self) -> bool:
"""Check if there are any errors."""
return any(rule.errors for rule in self.rules)
def css(self) -> str:
return "\n\n".join(rule_set.css for rule_set in self.rules)
@property
def error_renderable(self) -> StylesheetErrors:
@@ -129,6 +131,28 @@ class Stylesheet:
"""
self.variables = variables
def _parse_rules(self, css: str, path: str) -> list[RuleSet]:
"""Parse CSS and return rules.
Args:
css (str): String containing Textual CSS.
path (str): Path to CSS or unique identifier
Raises:
StylesheetError: If the CSS is invalid.
Returns:
list[RuleSet]: List of RuleSets.
"""
try:
rules = list(parse(css, path, variables=self.variables))
except TokenizeError:
raise
except Exception as error:
raise StylesheetError(f"failed to parse css; {error}")
return rules
def read(self, filename: str) -> None:
"""Read Textual CSS file.
@@ -146,19 +170,10 @@ class Stylesheet:
path = os.path.abspath(filename)
except Exception as error:
raise StylesheetError(f"unable to read {filename!r}; {error}")
try:
rules = list(parse(css, path, variables=self.variables))
except TokenizeError:
raise
except Exception as error:
raise StylesheetError(f"failed to parse {filename!r}; {error!r}")
else:
self.source.append((css, path))
self.rules.extend(rules)
if self.any_errors:
raise StylesheetParseError(self.error_renderable)
self.source[path] = css
self._rules = None
def parse(self, css: str, *, path: str = "") -> None:
def add_source(self, css: str, path: str | None = None) -> None:
"""Parse CSS from a string.
Args:
@@ -169,28 +184,30 @@ class Stylesheet:
StylesheetError: If the CSS could not be read.
StylesheetParseError: If the CSS is invalid.
"""
if (css, path) in self.source:
if path is None:
path = str(hash(css))
if path in self.source and self.source[path] == css:
# Path already in source, and CSS is identical
return
try:
rules = list(parse(css, path, variables=self.variables))
except TokenizeError:
raise
except Exception as error:
raise StylesheetError(f"failed to parse css; {error}")
else:
self.source.append((css, path))
self.rules.extend(rules)
if self.any_errors:
raise StylesheetParseError(self.error_renderable)
def _clone(self, stylesheet: Stylesheet) -> None:
"""Replace this stylesheet contents with another.
self.source[path] = css
self._rules = None
Args:
stylesheet (Stylesheet): A Stylesheet.
def parse(self) -> None:
"""Parse the source in the stylesheet.
Raises:
StylesheetParseError: If there are any CSS related errors.
"""
self.rules = stylesheet.rules.copy()
self.source = stylesheet.source.copy()
rules: list[RuleSet] = []
add_rules = rules.extend
for path, css in self.source.items():
css_rules = self._parse_rules(css, path)
if any(rule.errors for rule in css_rules):
raise StylesheetParseError(self.error_renderable)
add_rules(css_rules)
self._rules = rules
def reparse(self) -> None:
"""Re-parse source, applying new variables.
@@ -202,9 +219,10 @@ class Stylesheet:
"""
# Do this in a fresh Stylesheet so if there are errors we don't break self.
stylesheet = Stylesheet(variables=self.variables)
for css, path in self.source:
stylesheet.parse(css, path=path)
self._clone(stylesheet)
for path, css in self.source.items():
stylesheet.add_source(css, path)
self._rules = None
self.source = stylesheet.source
@classmethod
def _check_rule(cls, rule: RuleSet, node: DOMNode) -> Iterable[Specificity3]:
@@ -403,7 +421,7 @@ if __name__ == "__main__":
"""
stylesheet = Stylesheet()
stylesheet.parse(CSS)
stylesheet.add_source(CSS)
print(stylesheet.css)

View File

@@ -68,10 +68,6 @@ class Widget(DOMNode):
can_focus: bool = False
DEFAULT_STYLES = """
"""
CSS = """
"""
@@ -127,7 +123,9 @@ class Widget(DOMNode):
app (App): App instance.
"""
# Parser the Widget's CSS
self.app.stylesheet.parse(self.CSS, path=f"<{self.__class__.__name__}>")
self.app.stylesheet.add_source(
self.CSS, f"{__file__}:<{self.__class__.__name__}>"
)
def get_box_model(self, container: Size, viewport: Size) -> BoxModel:
"""Process the box model for this widget.

View File

@@ -23,9 +23,9 @@ class Button(Widget, can_focus=True):
background: $primary;
color: $text-primary;
content-align: center middle;
border: tall $primary-lighten-3;
margin: 1;
min-width:16;
border: tall $primary-lighten-3;
margin: 1 0;
text-style: bold;
}

View File

@@ -864,7 +864,7 @@ class TestParseLayout:
css = "#some-widget { layout: dock; }"
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
styles = stylesheet.rules[0].styles
assert isinstance(styles.layout, DockLayout)
@@ -874,7 +874,7 @@ class TestParseLayout:
stylesheet = Stylesheet()
with pytest.raises(StylesheetParseError) as ex:
stylesheet.parse(css)
stylesheet.add_source(css)
assert ex.value.errors is not None
@@ -886,7 +886,7 @@ class TestParseText:
}
"""
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
styles = stylesheet.rules[0].styles
assert styles.color == Color.parse("green")
@@ -897,7 +897,7 @@ class TestParseText:
}
"""
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
styles = stylesheet.rules[0].styles
assert styles.background == Color.parse("red")
@@ -933,7 +933,7 @@ class TestParseOffset:
}}
"""
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
styles = stylesheet.rules[0].styles
@@ -972,7 +972,7 @@ class TestParseOffset:
}}
"""
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
styles = stylesheet.rules[0].styles
@@ -1002,7 +1002,7 @@ class TestParseTransition:
}}
"""
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
styles = stylesheet.rules[0].styles
@@ -1017,7 +1017,7 @@ class TestParseTransition:
def test_no_delay_specified(self):
css = f"#some-widget {{ transition: offset-x 1 in_out_cubic; }}"
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
styles = stylesheet.rules[0].styles
@@ -1032,7 +1032,7 @@ class TestParseTransition:
stylesheet = Stylesheet()
with pytest.raises(StylesheetParseError) as ex:
stylesheet.parse(css)
stylesheet.add_source(css)
stylesheet_errors = stylesheet.rules[0].errors
@@ -1056,7 +1056,7 @@ class TestParseOpacity:
def test_opacity_to_styles(self, css_value, styles_value):
css = f"#some-widget {{ opacity: {css_value} }}"
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
assert stylesheet.rules[0].styles.opacity == styles_value
assert not stylesheet.rules[0].errors
@@ -1066,7 +1066,7 @@ class TestParseOpacity:
stylesheet = Stylesheet()
with pytest.raises(StylesheetParseError):
stylesheet.parse(css)
stylesheet.add_source(css)
assert stylesheet.rules[0].errors
@@ -1074,7 +1074,7 @@ class TestParseMargin:
def test_margin_partial(self):
css = "#foo {margin: 1; margin-top: 2; margin-right: 3; margin-bottom: -1;}"
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
assert stylesheet.rules[0].styles.margin == Spacing(2, 3, -1, 1)
@@ -1082,5 +1082,5 @@ class TestParsePadding:
def test_padding_partial(self):
css = "#foo {padding: 1; padding-top: 2; padding-right: 3; padding-bottom: -1;}"
stylesheet = Stylesheet()
stylesheet.parse(css)
stylesheet.add_source(css)
assert stylesheet.rules[0].styles.padding == Spacing(2, 3, -1, 1)

View File

@@ -42,7 +42,7 @@ def test_color_property_parsing(css_value, expectation, expected_color):
)
with expectation:
stylesheet.parse(css)
stylesheet.add_source(css)
if expected_color:
css_rule = stylesheet.rules[0]