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 {
|
#uber1 {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
|
|
||||||
background: dark_green;
|
background: green;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
border: heavy white;
|
border: heavy white;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class BasicApp(App):
|
|||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
|
|
||||||
def action_quit(self):
|
def action_quit(self):
|
||||||
self.panic(self.screen.tree)
|
self.panic(self.app.tree)
|
||||||
|
|
||||||
def action_dump(self):
|
def action_dump(self):
|
||||||
self.panic(str(self.app.registry))
|
self.panic(str(self.app.registry))
|
||||||
|
|||||||
@@ -330,10 +330,10 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
async def _on_css_change(self) -> None:
|
async def _on_css_change(self) -> None:
|
||||||
"""Called when the CSS changes (if watch_css is True)."""
|
"""Called when the CSS changes (if watch_css is True)."""
|
||||||
if self.css_file is not None:
|
if self.css_file is not None:
|
||||||
stylesheet = Stylesheet(variables=self.get_css_variables())
|
|
||||||
try:
|
try:
|
||||||
time = perf_counter()
|
time = perf_counter()
|
||||||
stylesheet.read(self.css_file)
|
self.stylesheet.read(self.css_file)
|
||||||
elapsed = (perf_counter() - time) * 1000
|
elapsed = (perf_counter() - time) * 1000
|
||||||
self.log(f"loaded {self.css_file} in {elapsed:.0f}ms")
|
self.log(f"loaded {self.css_file} in {elapsed:.0f}ms")
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
@@ -342,7 +342,6 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
self.log(error)
|
self.log(error)
|
||||||
else:
|
else:
|
||||||
self.reset_styles()
|
self.reset_styles()
|
||||||
self.stylesheet = stylesheet
|
|
||||||
self.stylesheet.update(self)
|
self.stylesheet.update(self)
|
||||||
self.screen.refresh(layout=True)
|
self.screen.refresh(layout=True)
|
||||||
|
|
||||||
@@ -506,7 +505,9 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
if self.css_file is not None:
|
if self.css_file is not None:
|
||||||
self.stylesheet.read(self.css_file)
|
self.stylesheet.read(self.css_file)
|
||||||
if self.css is not None:
|
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:
|
except Exception as error:
|
||||||
self.on_exception(error)
|
self.on_exception(error)
|
||||||
self._print_error_renderables()
|
self._print_error_renderables()
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ if __name__ == "__main__":
|
|||||||
console = Console()
|
console = Console()
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
try:
|
try:
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
except StylesheetParseError as e:
|
except StylesheetParseError as e:
|
||||||
console.print(e.errors)
|
console.print(e.errors)
|
||||||
print(stylesheet)
|
print(stylesheet)
|
||||||
|
|||||||
@@ -101,21 +101,23 @@ class StylesheetErrors:
|
|||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Stylesheet:
|
class Stylesheet:
|
||||||
def __init__(self, *, variables: dict[str, str] | None = None) -> None:
|
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.variables = variables or {}
|
||||||
self.source: list[tuple[str, str]] = []
|
self.source: dict[str, str] = {}
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield self.rules
|
yield self.rules
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def css(self) -> str:
|
def rules(self) -> list[RuleSet]:
|
||||||
return "\n\n".join(rule_set.css for rule_set in self.rules)
|
if self._rules is None:
|
||||||
|
self.parse()
|
||||||
|
assert self._rules is not None
|
||||||
|
return self._rules
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def any_errors(self) -> bool:
|
def css(self) -> str:
|
||||||
"""Check if there are any errors."""
|
return "\n\n".join(rule_set.css for rule_set in self.rules)
|
||||||
return any(rule.errors for rule in self.rules)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error_renderable(self) -> StylesheetErrors:
|
def error_renderable(self) -> StylesheetErrors:
|
||||||
@@ -129,6 +131,28 @@ class Stylesheet:
|
|||||||
"""
|
"""
|
||||||
self.variables = variables
|
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:
|
def read(self, filename: str) -> None:
|
||||||
"""Read Textual CSS file.
|
"""Read Textual CSS file.
|
||||||
|
|
||||||
@@ -146,19 +170,10 @@ class Stylesheet:
|
|||||||
path = os.path.abspath(filename)
|
path = os.path.abspath(filename)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise StylesheetError(f"unable to read {filename!r}; {error}")
|
raise StylesheetError(f"unable to read {filename!r}; {error}")
|
||||||
try:
|
self.source[path] = css
|
||||||
rules = list(parse(css, path, variables=self.variables))
|
self._rules = None
|
||||||
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)
|
|
||||||
|
|
||||||
def parse(self, css: str, *, path: str = "") -> None:
|
def add_source(self, css: str, path: str | None = None) -> None:
|
||||||
"""Parse CSS from a string.
|
"""Parse CSS from a string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -169,28 +184,30 @@ class Stylesheet:
|
|||||||
StylesheetError: If the CSS could not be read.
|
StylesheetError: If the CSS could not be read.
|
||||||
StylesheetParseError: If the CSS is invalid.
|
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
|
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:
|
self.source[path] = css
|
||||||
"""Replace this stylesheet contents with another.
|
self._rules = None
|
||||||
|
|
||||||
Args:
|
def parse(self) -> None:
|
||||||
stylesheet (Stylesheet): A Stylesheet.
|
"""Parse the source in the stylesheet.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
StylesheetParseError: If there are any CSS related errors.
|
||||||
"""
|
"""
|
||||||
self.rules = stylesheet.rules.copy()
|
rules: list[RuleSet] = []
|
||||||
self.source = stylesheet.source.copy()
|
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:
|
def reparse(self) -> None:
|
||||||
"""Re-parse source, applying new variables.
|
"""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.
|
# Do this in a fresh Stylesheet so if there are errors we don't break self.
|
||||||
stylesheet = Stylesheet(variables=self.variables)
|
stylesheet = Stylesheet(variables=self.variables)
|
||||||
for css, path in self.source:
|
for path, css in self.source.items():
|
||||||
stylesheet.parse(css, path=path)
|
stylesheet.add_source(css, path)
|
||||||
self._clone(stylesheet)
|
self._rules = None
|
||||||
|
self.source = stylesheet.source
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_rule(cls, rule: RuleSet, node: DOMNode) -> Iterable[Specificity3]:
|
def _check_rule(cls, rule: RuleSet, node: DOMNode) -> Iterable[Specificity3]:
|
||||||
@@ -403,7 +421,7 @@ if __name__ == "__main__":
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(CSS)
|
stylesheet.add_source(CSS)
|
||||||
|
|
||||||
print(stylesheet.css)
|
print(stylesheet.css)
|
||||||
|
|
||||||
|
|||||||
@@ -68,10 +68,6 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
can_focus: bool = False
|
can_focus: bool = False
|
||||||
|
|
||||||
DEFAULT_STYLES = """
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
CSS = """
|
CSS = """
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -127,7 +123,9 @@ class Widget(DOMNode):
|
|||||||
app (App): App instance.
|
app (App): App instance.
|
||||||
"""
|
"""
|
||||||
# Parser the Widget's CSS
|
# 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:
|
def get_box_model(self, container: Size, viewport: Size) -> BoxModel:
|
||||||
"""Process the box model for this widget.
|
"""Process the box model for this widget.
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ class Button(Widget, can_focus=True):
|
|||||||
background: $primary;
|
background: $primary;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
border: tall $primary-lighten-3;
|
border: tall $primary-lighten-3;
|
||||||
margin: 1;
|
|
||||||
min-width:16;
|
margin: 1 0;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -864,7 +864,7 @@ class TestParseLayout:
|
|||||||
css = "#some-widget { layout: dock; }"
|
css = "#some-widget { layout: dock; }"
|
||||||
|
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
assert isinstance(styles.layout, DockLayout)
|
assert isinstance(styles.layout, DockLayout)
|
||||||
@@ -874,7 +874,7 @@ class TestParseLayout:
|
|||||||
|
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
with pytest.raises(StylesheetParseError) as ex:
|
with pytest.raises(StylesheetParseError) as ex:
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
assert ex.value.errors is not None
|
assert ex.value.errors is not None
|
||||||
|
|
||||||
@@ -886,7 +886,7 @@ class TestParseText:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
assert styles.color == Color.parse("green")
|
assert styles.color == Color.parse("green")
|
||||||
@@ -897,7 +897,7 @@ class TestParseText:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
assert styles.background == Color.parse("red")
|
assert styles.background == Color.parse("red")
|
||||||
@@ -933,7 +933,7 @@ class TestParseOffset:
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
|
|
||||||
@@ -972,7 +972,7 @@ class TestParseOffset:
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
|
|
||||||
@@ -1002,7 +1002,7 @@ class TestParseTransition:
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
|
|
||||||
@@ -1017,7 +1017,7 @@ class TestParseTransition:
|
|||||||
def test_no_delay_specified(self):
|
def test_no_delay_specified(self):
|
||||||
css = f"#some-widget {{ transition: offset-x 1 in_out_cubic; }}"
|
css = f"#some-widget {{ transition: offset-x 1 in_out_cubic; }}"
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
|
|
||||||
@@ -1032,7 +1032,7 @@ class TestParseTransition:
|
|||||||
|
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
with pytest.raises(StylesheetParseError) as ex:
|
with pytest.raises(StylesheetParseError) as ex:
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
stylesheet_errors = stylesheet.rules[0].errors
|
stylesheet_errors = stylesheet.rules[0].errors
|
||||||
|
|
||||||
@@ -1056,7 +1056,7 @@ class TestParseOpacity:
|
|||||||
def test_opacity_to_styles(self, css_value, styles_value):
|
def test_opacity_to_styles(self, css_value, styles_value):
|
||||||
css = f"#some-widget {{ opacity: {css_value} }}"
|
css = f"#some-widget {{ opacity: {css_value} }}"
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
assert stylesheet.rules[0].styles.opacity == styles_value
|
assert stylesheet.rules[0].styles.opacity == styles_value
|
||||||
assert not stylesheet.rules[0].errors
|
assert not stylesheet.rules[0].errors
|
||||||
@@ -1066,7 +1066,7 @@ class TestParseOpacity:
|
|||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
|
|
||||||
with pytest.raises(StylesheetParseError):
|
with pytest.raises(StylesheetParseError):
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
assert stylesheet.rules[0].errors
|
assert stylesheet.rules[0].errors
|
||||||
|
|
||||||
|
|
||||||
@@ -1074,7 +1074,7 @@ class TestParseMargin:
|
|||||||
def test_margin_partial(self):
|
def test_margin_partial(self):
|
||||||
css = "#foo {margin: 1; margin-top: 2; margin-right: 3; margin-bottom: -1;}"
|
css = "#foo {margin: 1; margin-top: 2; margin-right: 3; margin-bottom: -1;}"
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
assert stylesheet.rules[0].styles.margin == Spacing(2, 3, -1, 1)
|
assert stylesheet.rules[0].styles.margin == Spacing(2, 3, -1, 1)
|
||||||
|
|
||||||
|
|
||||||
@@ -1082,5 +1082,5 @@ class TestParsePadding:
|
|||||||
def test_padding_partial(self):
|
def test_padding_partial(self):
|
||||||
css = "#foo {padding: 1; padding-top: 2; padding-right: 3; padding-bottom: -1;}"
|
css = "#foo {padding: 1; padding-top: 2; padding-right: 3; padding-bottom: -1;}"
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
assert stylesheet.rules[0].styles.padding == Spacing(2, 3, -1, 1)
|
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:
|
with expectation:
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
if expected_color:
|
if expected_color:
|
||||||
css_rule = stylesheet.rules[0]
|
css_rule = stylesheet.rules[0]
|
||||||
|
|||||||
Reference in New Issue
Block a user