Consistent error messaging across CSS and inline styles

This commit is contained in:
Darren Burns
2022-04-20 17:17:28 +01:00
parent 637b14126a
commit 92ef2e093e
10 changed files with 83 additions and 20 deletions

View File

@@ -10,5 +10,5 @@
height: 8;
min-width: 80;
background: dark_blue;
margin-bottom: 4;
padding: 1 2;
}

View File

@@ -87,8 +87,8 @@ class BasicApp(App):
def action_increase_margin(self):
old_margin = self.focused.styles.margin
new_margin = old_margin + Spacing.all(1)
self.focused.styles.margin = new_margin
# new_margin = old_margin + (1,1,1)
self.focused.styles.padding = (1, 1, 1)
BasicApp.run(css_file="uber.css", log="textual.log", log_verbosity=1)

View File

@@ -16,7 +16,7 @@ def friendly_list(words: Iterable[str], joiner: str = "or") -> str:
Returns:
str: List as prose.
"""
words = [repr(word) for word in sorted(words, key=str.lower)]
words = [repr(word) for word in sorted(words, key=str.lower) if word]
if len(words) == 1:
return words[0]
elif len(words) == 2:

View File

@@ -0,0 +1,31 @@
from textual.css._error_tools import friendly_list
from textual.css.scalar import SYMBOL_UNIT
def _inline_name(property_name: str) -> str:
return property_name.replace("-", "_")
def spacing_help_text(property_name: str, num_values_supplied: int) -> str:
return f"""\
• You supplied {num_values_supplied} values for the '{property_name}' property
• Spacing properties like 'margin' and 'padding' require either 1, 2 or 4 integer values
• In Textual CSS, supply 1, 2 or 4 values separated by a space
[dim]e.g. [/][i]{property_name}: 1 2 3 4;[/]
• In Python, you can set it to a tuple to assign spacing to each edge
[dim]e.g. [/][i]widget.styles.{_inline_name(property_name)} = (1, 2, 3, 4)[/]
• Or to an integer to assign to all edges at once
[dim]e.g. [/][i]widget.styles.{_inline_name(property_name)} = 2[/]"""
def scalar_help_text(property_name: str) -> str:
return f"""\
• Invalid value for the '{property_name}' property
• Scalar properties like '{property_name}' require numerical values and an optional unit
• Valid units are {friendly_list(SYMBOL_UNIT)}
• Here's an example of how you'd set a scalar property in Textual CSS
[dim]e.g. [/][i]{property_name}: 50%;[/]
• In Python, you can assign a string, int or Scalar object itself
[dim]e.g. [/][i]widget.styles.{_inline_name(property_name)} = "50%"[/]
[dim]e.g. [/][i]widget.styles.{_inline_name(property_name)} = 10[/]
[dim]e.g. [/][i]widget.styles.{_inline_name(property_name)} = Scalar(...)[/]"""

View File

@@ -9,11 +9,13 @@ when setting and getting.
from __future__ import annotations
import inspect
from typing import Iterable, NamedTuple, TYPE_CHECKING, cast
import rich.repr
from rich.style import Style
from ._help_text import spacing_help_text, scalar_help_text
from ..color import Color, ColorPair
from ._error_tools import friendly_list
from .constants import NULL_SPACING
@@ -98,7 +100,10 @@ class ScalarProperty:
try:
new_value = Scalar.parse(value)
except ScalarParseError:
raise StyleValueError("unable to parse scalar from {value!r}")
raise StyleValueError(
"unable to parse scalar from {value!r}",
help_text=scalar_help_text(property_name=self.name),
)
else:
raise StyleValueError("expected float, int, Scalar, or None")
if new_value is not None and new_value.unit not in self.units:
@@ -368,7 +373,16 @@ class SpacingProperty:
if obj.clear_rule(self.name):
obj.refresh(layout=True)
else:
if obj.set_rule(self.name, Spacing.unpack(spacing)):
try:
unpacked_spacing = Spacing.unpack(spacing)
except ValueError as error:
raise StyleValueError(
str(error),
help_text=spacing_help_text(
property_name=self.name, num_values_supplied=len(spacing)
),
)
if obj.set_rule(self.name, unpacked_spacing):
obj.refresh(layout=True)

View File

@@ -5,6 +5,7 @@ from typing import cast, Iterable, NoReturn
import rich.repr
from ._error_tools import friendly_list
from ._help_text import spacing_help_text, scalar_help_text
from .constants import (
VALID_BORDER,
VALID_BOX_SIZING,
@@ -15,7 +16,7 @@ from .constants import (
)
from .errors import DeclarationError
from .model import Declaration
from .scalar import Scalar, ScalarOffset, Unit, ScalarError
from .scalar import Scalar, ScalarOffset, Unit, ScalarError, ScalarParseError
from .styles import DockGroup, Styles
from .tokenize import Token
from .transition import Transition
@@ -160,12 +161,20 @@ class StylesBuilder:
self.error(name, token, f"invalid token {value!r} in this context")
def _process_scalar(self, name: str, tokens: list[Token]) -> None:
def scalar_error():
self.error(name, tokens[0], scalar_help_text(property_name=name))
if not tokens:
return
if len(tokens) == 1:
self.styles._rules[name.replace("-", "_")] = Scalar.parse(tokens[0].value)
try:
self.styles._rules[name.replace("-", "_")] = Scalar.parse(
tokens[0].value
)
except ScalarParseError:
scalar_error()
else:
self.error(name, tokens[0], "a single scalar is expected")
scalar_error()
def process_box_sizing(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
@@ -287,7 +296,7 @@ class StylesBuilder:
self.error(
name,
tokens[0],
f"1, 2, or 4 values expected; received {len(space)} values",
spacing_help_text(name, num_values_supplied=len(space)),
)
self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space)))

View File

@@ -31,4 +31,4 @@ VALID_BOX_SIZING: Final = {"border-box", "content-box"}
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
NULL_SPACING: Final = Spacing(0, 0, 0, 0)
NULL_SPACING: Final = Spacing.all(0)

View File

@@ -1,3 +1,9 @@
from __future__ import annotations
from rich.console import ConsoleOptions, Console
from rich.padding import Padding
from rich.traceback import Traceback
from .tokenize import Token
@@ -18,7 +24,13 @@ class StyleTypeError(TypeError):
class StyleValueError(ValueError):
pass
def __init__(self, *args, help_text: str | None = None):
super().__init__(*args)
self.help_text = help_text
def __rich_console__(self, console: Console, options: ConsoleOptions):
yield Traceback.from_exception(type(self), self, self.__traceback__)
yield Padding(self.help_text, pad=(1, 0, 0, 1))
class StylesheetError(Exception):

View File

@@ -10,6 +10,7 @@ from typing import cast, Iterable
import rich.repr
from rich.console import Group, RenderableType
from rich.highlighter import ReprHighlighter
from rich.markup import render
from rich.padding import Padding
from rich.panel import Panel
from rich.syntax import Syntax
@@ -88,13 +89,7 @@ class StylesheetErrors:
)
append(self._get_snippet(token.code, line_no))
final_message = ""
for is_last, message_part in loop_last(message.split(";")):
end = "" if is_last else "\n"
final_message += f"{message_part.strip()};{end}"
append(Padding(highlighter(Text(final_message, "red")), pad=(0, 1)))
append("")
append(Padding(highlighter(render(message)), pad=(0, 1)))
return Group(*errors)

View File

@@ -640,7 +640,9 @@ class Spacing(NamedTuple):
if pad_len == 4:
top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
return cls(top, right, bottom, left)
raise ValueError(f"1, 2 or 4 integers required for spacing; {pad_len} given")
raise ValueError(
f"1, 2 or 4 integers required for spacing properties; {pad_len} given"
)
@classmethod
def vertical(cls, amount: int) -> Spacing: