Skip to content

Commit ad83e91

Browse files
Merge pull request #3582 from Textualize/improve-css-error-reporting
Improve css error reporting
2 parents c19ffe3 + dfddeff commit ad83e91

File tree

15 files changed

+392
-346
lines changed

15 files changed

+392
-346
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

88

9+
## Unreleased
10+
11+
### Changed
12+
13+
- CSS error reporting will no longer provide links to the files in question https://github.com/Textualize/textual/pull/3582
14+
- inline CSS error reporting will report widget/class variable where the CSS was read from https://github.com/Textualize/textual/pull/3582
15+
916
## [0.41.0] - 2023-10-31
1017

1118
### Fixed

src/textual/app.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,20 +1771,20 @@ def _load_screen_css(self, screen: Screen):
17711771

17721772
update = False
17731773
for path in screen.css_path:
1774-
if not self.stylesheet.has_source(path):
1774+
if not self.stylesheet.has_source(str(path), ""):
17751775
self.stylesheet.read(path)
17761776
update = True
17771777
if screen.CSS:
17781778
try:
1779-
screen_css_path = (
1780-
f"{inspect.getfile(screen.__class__)}:{screen.__class__.__name__}"
1781-
)
1779+
screen_path = inspect.getfile(screen.__class__)
17821780
except (TypeError, OSError):
1783-
screen_css_path = f"{screen.__class__.__name__}"
1784-
if not self.stylesheet.has_source(screen_css_path):
1781+
screen_path = ""
1782+
screen_class_var = f"{screen.__class__.__name__}.CSS"
1783+
read_from = (screen_path, screen_class_var)
1784+
if not self.stylesheet.has_source(screen_path, screen_class_var):
17851785
self.stylesheet.add_source(
17861786
screen.CSS,
1787-
path=screen_css_path,
1787+
read_from=read_from,
17881788
is_default_css=False,
17891789
scope=screen._css_type_name if screen.SCOPED_CSS else "",
17901790
)
@@ -2145,23 +2145,22 @@ async def _process_messages(
21452145
try:
21462146
if self.css_path:
21472147
self.stylesheet.read_all(self.css_path)
2148-
for path, css, tie_breaker, scope in self._get_default_css():
2148+
for read_from, css, tie_breaker, scope in self._get_default_css():
21492149
self.stylesheet.add_source(
21502150
css,
2151-
path=path,
2151+
read_from=read_from,
21522152
is_default_css=True,
21532153
tie_breaker=tie_breaker,
21542154
scope=scope,
21552155
)
21562156
if self.CSS:
21572157
try:
2158-
app_css_path = (
2159-
f"{inspect.getfile(self.__class__)}:{self.__class__.__name__}"
2160-
)
2158+
app_path = inspect.getfile(self.__class__)
21612159
except (TypeError, OSError):
2162-
app_css_path = f"{self.__class__.__name__}"
2160+
app_path = ""
2161+
read_from = (app_path, f"{self.__class__.__name__}.CSS")
21632162
self.stylesheet.add_source(
2164-
self.CSS, path=app_css_path, is_default_css=False
2163+
self.CSS, read_from=read_from, is_default_css=False
21652164
)
21662165
except Exception as error:
21672166
self._handle_exception(error)

src/textual/css/_styles_builder.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,6 @@
6565
from .types import BoxSizing, Display, EdgeType, Overflow, Visibility
6666

6767

68-
def _join_tokens(tokens: Iterable[Token], joiner: str = "") -> str:
69-
"""Convert tokens into a string by joining their values
70-
71-
Args:
72-
tokens: Tokens to join
73-
joiner: String to join on.
74-
75-
Returns:
76-
The tokens, joined together to form a string.
77-
"""
78-
return joiner.join(token.value for token in tokens)
79-
80-
8168
class StylesBuilder:
8269
"""
8370
The StylesBuilder object takes tokens parsed from the CSS and converts

src/textual/css/parse.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
from functools import lru_cache
4-
from pathlib import PurePath
54
from typing import Iterable, Iterator, NoReturn
65

76
from ..suggestions import get_suggestion
@@ -19,7 +18,7 @@
1918
from .styles import Styles
2019
from .tokenize import Token, tokenize, tokenize_declarations, tokenize_values
2120
from .tokenizer import EOFError, ReferencedBy
22-
from .types import Specificity3
21+
from .types import CSSLocation, Specificity3
2322

2423
SELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = {
2524
"selector": (SelectorType.TYPE, (0, 0, 1)),
@@ -38,7 +37,7 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
3837
if not css_selectors.strip():
3938
return ()
4039

41-
tokens = iter(tokenize(css_selectors, ""))
40+
tokens = iter(tokenize(css_selectors, ("", "")))
4241

4342
get_selector = SELECTOR_MAP.get
4443
combinator: CombinatorType | None = CombinatorType.DESCENDENT
@@ -180,18 +179,18 @@ def parse_rule_set(
180179
yield rule_set
181180

182181

183-
def parse_declarations(css: str, path: str) -> Styles:
182+
def parse_declarations(css: str, read_from: CSSLocation) -> Styles:
184183
"""Parse declarations and return a Styles object.
185184
186185
Args:
187186
css: String containing CSS.
188-
path: Path to the CSS, or something else to identify the location.
187+
read_from: The location where the CSS was read from.
189188
190189
Returns:
191190
A styles object.
192191
"""
193192

194-
tokens = iter(tokenize_declarations(css, path))
193+
tokens = iter(tokenize_declarations(css, read_from))
195194
styles_builder = StylesBuilder()
196195

197196
declaration: Declaration | None = None
@@ -245,7 +244,7 @@ def _unresolved(variable_name: str, variables: Iterable[str], token: Token) -> N
245244
message += f"; did you mean '${suggested_variable}'?"
246245

247246
raise UnresolvedVariableError(
248-
token.path,
247+
token.read_from,
249248
token.code,
250249
token.start,
251250
message,
@@ -341,7 +340,7 @@ def substitute_references(
341340
def parse(
342341
scope: str,
343342
css: str,
344-
path: str | PurePath,
343+
read_from: CSSLocation,
345344
variables: dict[str, str] | None = None,
346345
variable_tokens: dict[str, list[Token]] | None = None,
347346
is_default_rules: bool = False,
@@ -351,9 +350,9 @@ def parse(
351350
and generating rule sets from it.
352351
353352
Args:
354-
scope: CSS type name
355-
css: The input CSS
356-
path: Path to the CSS
353+
scope: CSS type name.
354+
css: The input CSS.
355+
read_from: The source location of the CSS.
357356
variables: Substitution variables to substitute tokens for.
358357
is_default_rules: True if the rules we're extracting are
359358
default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS.
@@ -363,7 +362,7 @@ def parse(
363362
if variable_tokens:
364363
reference_tokens.update(variable_tokens)
365364

366-
tokens = iter(substitute_references(tokenize(css, path), variable_tokens))
365+
tokens = iter(substitute_references(tokenize(css, read_from), variable_tokens))
367366
while True:
368367
token = next(tokens, None)
369368
if token is None:

src/textual/css/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ def set_styles(
407407
node.set_styles(**update_styles)
408408
if css is not None:
409409
try:
410-
new_styles = parse_declarations(css, path="set_styles")
410+
new_styles = parse_declarations(css, read_from=("set_styles", ""))
411411
except DeclarationError as error:
412412
raise DeclarationError(error.name, error.token, error.message) from None
413413
for node in self:

src/textual/css/styles.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
if TYPE_CHECKING:
7070
from .._layout import Layout
7171
from ..dom import DOMNode
72+
from .types import CSSLocation
7273

7374

7475
class RulesMap(TypedDict, total=False):
@@ -534,20 +535,22 @@ def is_animatable(cls, rule: str) -> bool:
534535

535536
@classmethod
536537
@lru_cache(maxsize=1024)
537-
def parse(cls, css: str, path: str, *, node: DOMNode | None = None) -> Styles:
538+
def parse(
539+
cls, css: str, read_from: CSSLocation, *, node: DOMNode | None = None
540+
) -> Styles:
538541
"""Parse CSS and return a Styles object.
539542
540543
Args:
541544
css: Textual CSS.
542-
path: Path or string indicating source of CSS.
545+
read_from: Location where the CSS was read from.
543546
node: Node to associate with the Styles.
544547
545548
Returns:
546549
A Styles instance containing result of parsing CSS.
547550
"""
548551
from .parse import parse_declarations
549552

550-
styles = parse_declarations(css, path)
553+
styles = parse_declarations(css, read_from)
551554
styles.node = node
552555
return styles
553556

0 commit comments

Comments
 (0)