mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Consistent error messaging across CSS and inline styles
This commit is contained in:
@@ -10,5 +10,5 @@
|
||||
height: 8;
|
||||
min-width: 80;
|
||||
background: dark_blue;
|
||||
margin-bottom: 4;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
31
src/textual/css/_help_text.py
Normal file
31
src/textual/css/_help_text.py
Normal 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(...)[/]"""
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user