Feedback from code review

This commit is contained in:
Darren Burns
2022-06-29 13:30:21 +01:00
parent c00de10262
commit 908e2e940a
6 changed files with 75 additions and 50 deletions

View File

@@ -156,7 +156,7 @@ class RuleSet:
styles: Styles = field(default_factory=Styles) styles: Styles = field(default_factory=Styles)
errors: list[tuple[Token, str]] = field(default_factory=list) errors: list[tuple[Token, str]] = field(default_factory=list)
classes: set[str] = field(default_factory=set) classes: set[str] = field(default_factory=set)
is_widget_rule_set: bool = False is_default_rules: bool = False
@classmethod @classmethod
def _selector_to_css(cls, selectors: list[Selector]) -> str: def _selector_to_css(cls, selectors: list[Selector]) -> str:

View File

@@ -80,7 +80,9 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
return selector_set return selector_set
def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]: def parse_rule_set(
tokens: Iterator[Token], token: Token, is_default_rules: bool = False
) -> Iterable[RuleSet]:
get_selector = SELECTOR_MAP.get get_selector = SELECTOR_MAP.get
combinator: CombinatorType | None = CombinatorType.DESCENDENT combinator: CombinatorType | None = CombinatorType.DESCENDENT
selectors: list[Selector] = [] selectors: list[Selector] = []
@@ -149,7 +151,10 @@ def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
errors.append((error.token, error.message)) errors.append((error.token, error.message))
rule_set = RuleSet( rule_set = RuleSet(
list(SelectorSet.from_selectors(rule_selectors)), styles_builder.styles, errors list(SelectorSet.from_selectors(rule_selectors)),
styles_builder.styles,
errors,
is_default_rules=is_default_rules,
) )
rule_set._post_parse() rule_set._post_parse()
yield rule_set yield rule_set
@@ -307,7 +312,10 @@ def substitute_references(
def parse( def parse(
css: str, path: str | PurePath, variables: dict[str, str] | None = None css: str,
path: str | PurePath,
variables: dict[str, str] | None = None,
is_default_rules: bool = False,
) -> Iterable[RuleSet]: ) -> Iterable[RuleSet]:
"""Parse CSS by tokenizing it, performing variable substitution, """Parse CSS by tokenizing it, performing variable substitution,
and generating rule sets from it. and generating rule sets from it.
@@ -315,6 +323,9 @@ def parse(
Args: Args:
css (str): The input CSS css (str): The input CSS
path (str): Path to the CSS path (str): Path to the CSS
variables (dict[str, str]): Substitution variables to substitute tokens for.
is_default_rules (bool): True if the rules we're extracting are
default (i.e. in Widget.CSS) rules. False if they're from user defined CSS.
""" """
variable_tokens = tokenize_values(variables or {}) variable_tokens = tokenize_values(variables or {})
tokens = iter(substitute_references(tokenize(css, path), variable_tokens)) tokens = iter(substitute_references(tokenize(css, path), variable_tokens))
@@ -323,7 +334,7 @@ def parse(
if token is None: if token is None:
break break
if token.name.startswith("selector_start"): if token.name.startswith("selector_start"):
yield from parse_rule_set(tokens, token) yield from parse_rule_set(tokens, token, is_default_rules=is_default_rules)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -514,13 +514,15 @@ class Styles(StylesBase):
def extract_rules( def extract_rules(
self, self,
specificity: Specificity3, specificity: Specificity3,
is_widget_rule: bool = False, is_default_rules: bool = False,
) -> list[tuple[str, Specificity5, Any]]: ) -> list[tuple[str, Specificity5, Any]]:
"""Extract rules from Styles object, and apply !important css specificity as """Extract rules from Styles object, and apply !important css specificity as
well as higher specificity of user CSS vs widget CSS. well as higher specificity of user CSS vs widget CSS.
Args: Args:
specificity (Specificity3): A node specificity. specificity (Specificity3): A node specificity.
is_default_rules (bool): True if the rules we're extracting are
default (i.e. in Widget.CSS) rules. False if they're from user defined CSS.
Returns: Returns:
list[tuple[str, Specificity5, Any]]]: A list containing a tuple of <RULE NAME>, <SPECIFICITY> <RULE VALUE>. list[tuple[str, Specificity5, Any]]]: A list containing a tuple of <RULE NAME>, <SPECIFICITY> <RULE VALUE>.
@@ -531,7 +533,7 @@ class Styles(StylesBase):
( (
rule_name, rule_name,
( (
0 if is_widget_rule else 1, 0 if is_default_rules else 1,
1 if is_important(rule_name) else 0, 1 if is_important(rule_name) else 0,
*specificity, *specificity,
), ),

View File

@@ -4,7 +4,7 @@ import os
from collections import defaultdict from collections import defaultdict
from operator import itemgetter from operator import itemgetter
from pathlib import Path, PurePath from pathlib import Path, PurePath
from typing import cast, Iterable from typing import cast, Iterable, NamedTuple
import rich.repr import rich.repr
from rich.console import RenderableType, RenderResult, Console, ConsoleOptions from rich.console import RenderableType, RenderResult, Console, ConsoleOptions
@@ -116,14 +116,26 @@ class StylesheetErrors:
) )
class CssSource(NamedTuple):
"""Contains the CSS content and whether or not the CSS comes from user defined stylesheets
vs widget-level stylesheets.
Args:
content (str): The CSS as a string.
is_defaults (bool): True if the CSS is default (i.e. that defined at the widget level).
False if it's user CSS (which will override the defaults).
"""
content: str
is_defaults: bool
@rich.repr.auto(angular=True) @rich.repr.auto(angular=True)
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] = []
self.variables = variables or {} self.variables = variables or {}
self.source: dict[str, str] = {} self.source: dict[str, CssSource] = {}
# Records which of the source keys represent CSS defined at the widget level
self._widget_css_paths: set[str] = set()
self._require_parse = False self._require_parse = False
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
@@ -149,7 +161,6 @@ class Stylesheet:
""" """
stylesheet = Stylesheet(variables=self.variables.copy()) stylesheet = Stylesheet(variables=self.variables.copy())
stylesheet.source = self.source.copy() stylesheet.source = self.source.copy()
stylesheet._widget_css_paths = self._widget_css_paths.copy()
return stylesheet return stylesheet
def set_variables(self, variables: dict[str, str]) -> None: def set_variables(self, variables: dict[str, str]) -> None:
@@ -160,12 +171,17 @@ class Stylesheet:
""" """
self.variables = variables self.variables = variables
def _parse_rules(self, css: str, path: str | PurePath) -> list[RuleSet]: def _parse_rules(
self, css: str, path: str | PurePath, is_default_rules: bool = False
) -> list[RuleSet]:
"""Parse CSS and return rules. """Parse CSS and return rules.
Args: Args:
is_default_rules:
css (str): String containing Textual CSS. css (str): String containing Textual CSS.
path (str | PurePath): Path to CSS or unique identifier path (str | PurePath): Path to CSS or unique identifier
is_default_rules (bool): True if the rules we're extracting are
default (i.e. in Widget.CSS) rules. False if they're from user defined CSS.
Raises: Raises:
StylesheetError: If the CSS is invalid. StylesheetError: If the CSS is invalid.
@@ -174,16 +190,19 @@ class Stylesheet:
list[RuleSet]: List of RuleSets. list[RuleSet]: List of RuleSets.
""" """
try: try:
rules = list(parse(css, path, variables=self.variables)) rules = list(
parse(
css,
path,
variables=self.variables,
is_default_rules=is_default_rules,
)
)
except TokenizeError: except TokenizeError:
raise raise
except Exception as error: except Exception as error:
raise StylesheetError(f"failed to parse css; {error}") raise StylesheetError(f"failed to parse css; {error}")
if path in self._widget_css_paths:
for rule in rules:
rule.is_widget_rule_set = True
return rules return rules
def read(self, filename: str | PurePath) -> None: def read(self, filename: str | PurePath) -> None:
@@ -203,11 +222,11 @@ 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}")
self.source[str(path)] = css self.source[str(path)] = CssSource(content=css, is_defaults=False)
self._require_parse = True self._require_parse = True
def add_source( def add_source(
self, css: str, path: str | PurePath | None = None, is_widget_css: bool = False self, css: str, path: str | PurePath | None = None, is_default_css: bool = False
) -> None: ) -> None:
"""Parse CSS from a string. """Parse CSS from a string.
@@ -215,7 +234,7 @@ class Stylesheet:
css (str): String with CSS source. css (str): String with CSS source.
path (str | PurePath, optional): The path of the source if a file, or some other identifier. path (str | PurePath, optional): The path of the source if a file, or some other identifier.
Defaults to None. Defaults to None.
is_widget_css (bool): True if the CSS is defined in the Widget, False if the CSS is defined is_default_css (bool): True if the CSS is defined in the Widget, False if the CSS is defined
in a user stylesheet. in a user stylesheet.
Raises: Raises:
@@ -227,16 +246,11 @@ class Stylesheet:
path = str(hash(css)) path = str(hash(css))
elif isinstance(path, PurePath): elif isinstance(path, PurePath):
path = str(css) path = str(css)
if path in self.source and self.source[path] == css: if path in self.source and self.source[path].content == css:
# Path already in source, and CSS is identical # Path already in source, and CSS is identical
return return
# Record any CSS defined at the widget level for self.source[path] = CssSource(content=css, is_defaults=is_default_css)
# different specificity treatment from user CSS.
if is_widget_css:
self._widget_css_paths.add(path)
self.source[path] = css
self._require_parse = True self._require_parse = True
def parse(self) -> None: def parse(self) -> None:
@@ -247,8 +261,8 @@ class Stylesheet:
""" """
rules: list[RuleSet] = [] rules: list[RuleSet] = []
add_rules = rules.extend add_rules = rules.extend
for path, css in self.source.items(): for path, (css, is_default_rules) in self.source.items():
css_rules = self._parse_rules(css, path) css_rules = self._parse_rules(css, path, is_default_rules=is_default_rules)
if any(rule.errors for rule in css_rules): if any(rule.errors for rule in css_rules):
error_renderable = StylesheetErrors(css_rules) error_renderable = StylesheetErrors(css_rules)
raise StylesheetParseError(error_renderable) raise StylesheetParseError(error_renderable)
@@ -266,8 +280,8 @@ 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 path, css in self.source.items(): for path, (css, is_defaults) in self.source.items():
stylesheet.add_source(css, path) stylesheet.add_source(css, path, is_default_css=is_defaults)
stylesheet.parse() stylesheet.parse()
self._rules = stylesheet.rules self._rules = stylesheet.rules
self.source = stylesheet.source self.source = stylesheet.source
@@ -302,10 +316,10 @@ class Stylesheet:
# Collect the rules defined in the stylesheet # Collect the rules defined in the stylesheet
for rule in reversed(self.rules): for rule in reversed(self.rules):
is_widget_rule = rule.is_widget_rule_set is_default_rules = rule.is_default_rules
for base_specificity in _check_rule(rule, node): for base_specificity in _check_rule(rule, node):
for key, rule_specificity, value in rule.styles.extract_rules( for key, rule_specificity, value in rule.styles.extract_rules(
base_specificity, is_widget_rule base_specificity, is_default_rules
): ):
rule_attributes[key].append((rule_specificity, value)) rule_attributes[key].append((rule_specificity, value))

View File

@@ -194,7 +194,7 @@ class Widget(DOMNode):
""" """
# Parse the Widget's CSS # Parse the Widget's CSS
for path, css in self.css: for path, css in self.css:
self.app.stylesheet.add_source(css, path=path, is_widget_css=True) self.app.stylesheet.add_source(css, path=path, is_default_css=True)
def get_box_model( def get_box_model(
self, container: Size, viewport: Size, fraction_unit: Fraction self, container: Size, viewport: Size, fraction_unit: Fraction

View File

@@ -3,20 +3,18 @@ from typing import Any
import pytest import pytest
from tests.utilities.test_app import AppTest
from textual.app import App, ComposeResult
from textual.color import Color from textual.color import Color
from textual.css._help_renderables import HelpText from textual.css._help_renderables import HelpText
from textual.css.stylesheet import Stylesheet, StylesheetParseError from textual.css.stylesheet import Stylesheet, StylesheetParseError, CssSource
from textual.css.tokenizer import TokenizeError from textual.css.tokenizer import TokenizeError
from textual.dom import DOMNode from textual.dom import DOMNode
from textual.geometry import Spacing from textual.geometry import Spacing
from textual.widget import Widget from textual.widget import Widget
def _make_stylesheet(css: str) -> Stylesheet: def _make_user_stylesheet(css: str) -> Stylesheet:
stylesheet = Stylesheet() stylesheet = Stylesheet()
stylesheet.source["test.css"] = css stylesheet.source["test.css"] = CssSource(css, is_defaults=False)
stylesheet.parse() stylesheet.parse()
return stylesheet return stylesheet
@@ -24,7 +22,7 @@ def _make_stylesheet(css: str) -> Stylesheet:
def test_stylesheet_apply_highest_specificity_wins(): def test_stylesheet_apply_highest_specificity_wins():
"""#ids have higher specificity than .classes""" """#ids have higher specificity than .classes"""
css = "#id {color: red;} .class {color: blue;}" css = "#id {color: red;} .class {color: blue;}"
stylesheet = _make_stylesheet(css) stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="class", id="id") node = DOMNode(classes="class", id="id")
stylesheet.apply(node) stylesheet.apply(node)
@@ -33,7 +31,7 @@ def test_stylesheet_apply_highest_specificity_wins():
def test_stylesheet_apply_doesnt_override_defaults(): def test_stylesheet_apply_doesnt_override_defaults():
css = "#id {color: red;}" css = "#id {color: red;}"
stylesheet = _make_stylesheet(css) stylesheet = _make_user_stylesheet(css)
node = DOMNode(id="id") node = DOMNode(id="id")
stylesheet.apply(node) stylesheet.apply(node)
@@ -45,7 +43,7 @@ def test_stylesheet_apply_highest_specificity_wins_multiple_classes():
"""When we use two selectors containing only classes, then the selector """When we use two selectors containing only classes, then the selector
`.b.c` has greater specificity than the selector `.a`""" `.b.c` has greater specificity than the selector `.a`"""
css = ".b.c {background: blue;} .a {background: red; color: lime;}" css = ".b.c {background: blue;} .a {background: red; color: lime;}"
stylesheet = _make_stylesheet(css) stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="a b c") node = DOMNode(classes="a b c")
stylesheet.apply(node) stylesheet.apply(node)
@@ -58,7 +56,7 @@ def test_stylesheet_many_classes_dont_overrule_id():
a selector containing multiple classes cannot take priority over even a a selector containing multiple classes cannot take priority over even a
single class.""" single class."""
css = "#id {color: red;} .a.b.c.d {color: blue;}" css = "#id {color: red;} .a.b.c.d {color: blue;}"
stylesheet = _make_stylesheet(css) stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="a b c d", id="id") node = DOMNode(classes="a b c d", id="id")
stylesheet.apply(node) stylesheet.apply(node)
@@ -67,7 +65,7 @@ def test_stylesheet_many_classes_dont_overrule_id():
def test_stylesheet_last_rule_wins_when_same_rule_twice_in_one_ruleset(): def test_stylesheet_last_rule_wins_when_same_rule_twice_in_one_ruleset():
css = "#id {color: red; color: blue;}" css = "#id {color: red; color: blue;}"
stylesheet = _make_stylesheet(css) stylesheet = _make_user_stylesheet(css)
node = DOMNode(id="id") node = DOMNode(id="id")
stylesheet.apply(node) stylesheet.apply(node)
@@ -76,7 +74,7 @@ def test_stylesheet_last_rule_wins_when_same_rule_twice_in_one_ruleset():
def test_stylesheet_rulesets_merged_for_duplicate_selectors(): def test_stylesheet_rulesets_merged_for_duplicate_selectors():
css = "#id {color: red; background: lime;} #id {color:blue;}" css = "#id {color: red; background: lime;} #id {color:blue;}"
stylesheet = _make_stylesheet(css) stylesheet = _make_user_stylesheet(css)
node = DOMNode(id="id") node = DOMNode(id="id")
stylesheet.apply(node) stylesheet.apply(node)
@@ -88,7 +86,7 @@ def test_stylesheet_apply_takes_final_rule_in_specificity_clash():
""".a and .b both contain background and have same specificity, so .b wins """.a and .b both contain background and have same specificity, so .b wins
since it was declared last - the background should be blue.""" since it was declared last - the background should be blue."""
css = ".a {background: red; color: lime;} .b {background: blue;}" css = ".a {background: red; color: lime;} .b {background: blue;}"
stylesheet = _make_stylesheet(css) stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="a b", id="c") node = DOMNode(classes="a b", id="c")
stylesheet.apply(node) stylesheet.apply(node)
@@ -99,7 +97,7 @@ def test_stylesheet_apply_takes_final_rule_in_specificity_clash():
def test_stylesheet_apply_empty_rulesets(): def test_stylesheet_apply_empty_rulesets():
"""Ensure that we don't crash when working with empty rulesets""" """Ensure that we don't crash when working with empty rulesets"""
css = ".a {} .b {}" css = ".a {} .b {}"
stylesheet = _make_stylesheet(css) stylesheet = _make_user_stylesheet(css)
node = DOMNode(classes="a b") node = DOMNode(classes="a b")
stylesheet.apply(node) stylesheet.apply(node)
@@ -113,8 +111,8 @@ def test_stylesheet_apply_user_css_over_widget_css():
node = MyWidget() node = MyWidget()
node.add_class("a") node.add_class("a")
stylesheet = _make_stylesheet(user_css) stylesheet = _make_user_stylesheet(user_css)
stylesheet.add_source(MyWidget.CSS, "widget.py:MyWidget", is_widget_css=True) stylesheet.add_source(MyWidget.CSS, "widget.py:MyWidget", is_default_css=True)
stylesheet.apply(node) stylesheet.apply(node)
# The node is red because user CSS overrides Widget.CSS # The node is red because user CSS overrides Widget.CSS