mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix css reload
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#uber1 {
|
||||
layout: vertical;
|
||||
|
||||
background: dark_green;
|
||||
background: green;
|
||||
overflow: hidden auto;
|
||||
border: heavy white;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -24,8 +24,8 @@ class Button(Widget, can_focus=True):
|
||||
color: $text-primary;
|
||||
content-align: center middle;
|
||||
border: tall $primary-lighten-3;
|
||||
margin: 1;
|
||||
min-width:16;
|
||||
|
||||
margin: 1 0;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user