mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #486 from Textualize/css-add-did-you-mean-for-css-color-names
[css] Add a "Did you mean" suggestion when the value of a color is wrong but we can find a close one
This commit is contained in:
@@ -224,4 +224,4 @@ Success {
|
|||||||
|
|
||||||
.horizontal {
|
.horizontal {
|
||||||
layout: horizontal
|
layout: horizontal
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from rich.color import Color as RichColor
|
|||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
|
from textual.suggestions import get_suggestion
|
||||||
from ._color_constants import COLOR_NAME_TO_RGB
|
from ._color_constants import COLOR_NAME_TO_RGB
|
||||||
from .geometry import clamp
|
from .geometry import clamp
|
||||||
|
|
||||||
@@ -77,6 +77,17 @@ split_pairs4: Callable[[str], tuple[str, str, str, str]] = itemgetter(
|
|||||||
class ColorParseError(Exception):
|
class ColorParseError(Exception):
|
||||||
"""A color failed to parse"""
|
"""A color failed to parse"""
|
||||||
|
|
||||||
|
def __init__(self, message: str, suggested_color: str | None = None):
|
||||||
|
"""
|
||||||
|
Creates a new ColorParseError
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (str): the error message
|
||||||
|
suggested_color (str | None): a close color we can suggest. Defaults to None.
|
||||||
|
"""
|
||||||
|
super().__init__(message)
|
||||||
|
self.suggested_color = suggested_color
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Color(NamedTuple):
|
class Color(NamedTuple):
|
||||||
@@ -271,7 +282,14 @@ class Color(NamedTuple):
|
|||||||
return cls(*color_from_name)
|
return cls(*color_from_name)
|
||||||
color_match = RE_COLOR.match(color_text)
|
color_match = RE_COLOR.match(color_text)
|
||||||
if color_match is None:
|
if color_match is None:
|
||||||
raise ColorParseError(f"failed to parse {color_text!r} as a color")
|
error_message = f"failed to parse {color_text!r} as a color"
|
||||||
|
suggested_color = None
|
||||||
|
if not color_text.startswith("#") and not color_text.startswith("rgb"):
|
||||||
|
# Seems like we tried to use a color name: let's try to find one that is close enough:
|
||||||
|
suggested_color = get_suggestion(color_text, COLOR_NAME_TO_RGB.keys())
|
||||||
|
if suggested_color:
|
||||||
|
error_message += f"; did you mean '{suggested_color}'?"
|
||||||
|
raise ColorParseError(error_message, suggested_color)
|
||||||
(
|
(
|
||||||
rgb_hex_triple,
|
rgb_hex_triple,
|
||||||
rgb_hex_quad,
|
rgb_hex_quad,
|
||||||
|
|||||||
@@ -70,13 +70,13 @@ class HelpText:
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
summary (str): A succinct summary of the issue.
|
summary (str): A succinct summary of the issue.
|
||||||
bullets (Iterable[Bullet]): Bullet points which provide additional
|
bullets (Iterable[Bullet] | None): Bullet points which provide additional
|
||||||
context around the issue. These are rendered below the summary.
|
context around the issue. These are rendered below the summary. Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, summary: str, *, bullets: Iterable[Bullet]) -> None:
|
def __init__(self, summary: str, *, bullets: Iterable[Bullet] = None) -> None:
|
||||||
self.summary = summary
|
self.summary = summary
|
||||||
self.bullets = bullets
|
self.bullets = bullets or []
|
||||||
|
|
||||||
def __rich_console__(
|
def __rich_console__(
|
||||||
self, console: Console, options: ConsoleOptions
|
self, console: Console, options: ConsoleOptions
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import sys
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
|
from textual.color import ColorParseError
|
||||||
from textual.css._help_renderables import Example, Bullet, HelpText
|
from textual.css._help_renderables import Example, Bullet, HelpText
|
||||||
from textual.css.constants import (
|
from textual.css.constants import (
|
||||||
VALID_BORDER,
|
VALID_BORDER,
|
||||||
@@ -144,13 +145,13 @@ def property_invalid_value_help_text(
|
|||||||
HelpText: Renderable for displaying the help text for this property
|
HelpText: Renderable for displaying the help text for this property
|
||||||
"""
|
"""
|
||||||
property_name = _contextualize_property_name(property_name, context)
|
property_name = _contextualize_property_name(property_name, context)
|
||||||
bullets = []
|
summary = f"Invalid CSS property [i]{property_name}[/]"
|
||||||
if suggested_property_name:
|
if suggested_property_name:
|
||||||
suggested_property_name = _contextualize_property_name(
|
suggested_property_name = _contextualize_property_name(
|
||||||
suggested_property_name, context
|
suggested_property_name, context
|
||||||
)
|
)
|
||||||
bullets.append(Bullet(f'Did you mean "{suggested_property_name}"?'))
|
summary += f'. Did you mean "{suggested_property_name}"?'
|
||||||
return HelpText(f"Invalid CSS property [i]{property_name}[/]", bullets=bullets)
|
return HelpText(summary)
|
||||||
|
|
||||||
|
|
||||||
def spacing_wrong_number_of_values_help_text(
|
def spacing_wrong_number_of_values_help_text(
|
||||||
@@ -303,6 +304,8 @@ def string_enum_help_text(
|
|||||||
def color_property_help_text(
|
def color_property_help_text(
|
||||||
property_name: str,
|
property_name: str,
|
||||||
context: StylingContext,
|
context: StylingContext,
|
||||||
|
*,
|
||||||
|
error: Exception = None,
|
||||||
) -> HelpText:
|
) -> HelpText:
|
||||||
"""Help text to show when the user supplies an invalid value for a color
|
"""Help text to show when the user supplies an invalid value for a color
|
||||||
property. For example, an unparseable color string.
|
property. For example, an unparseable color string.
|
||||||
@@ -310,13 +313,20 @@ def color_property_help_text(
|
|||||||
Args:
|
Args:
|
||||||
property_name (str): The name of the property
|
property_name (str): The name of the property
|
||||||
context (StylingContext | None): The context the property is being used in.
|
context (StylingContext | None): The context the property is being used in.
|
||||||
|
error (ColorParseError | None): The error that caused this help text to be displayed. Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
HelpText: Renderable for displaying the help text for this property
|
HelpText: Renderable for displaying the help text for this property
|
||||||
"""
|
"""
|
||||||
property_name = _contextualize_property_name(property_name, context)
|
property_name = _contextualize_property_name(property_name, context)
|
||||||
|
summary = f"Invalid value for the [i]{property_name}[/] property"
|
||||||
|
suggested_color = (
|
||||||
|
error.suggested_color if error and isinstance(error, ColorParseError) else None
|
||||||
|
)
|
||||||
|
if suggested_color:
|
||||||
|
summary += f'. Did you mean "{suggested_color}"?'
|
||||||
return HelpText(
|
return HelpText(
|
||||||
summary=f"Invalid value for the [i]{property_name}[/] property",
|
summary=summary,
|
||||||
bullets=[
|
bullets=[
|
||||||
Bullet(
|
Bullet(
|
||||||
f"The [i]{property_name}[/] property can only be set to a valid color"
|
f"The [i]{property_name}[/] property can only be set to a valid color"
|
||||||
|
|||||||
@@ -782,10 +782,12 @@ class ColorProperty:
|
|||||||
elif isinstance(color, str):
|
elif isinstance(color, str):
|
||||||
try:
|
try:
|
||||||
parsed_color = Color.parse(color)
|
parsed_color = Color.parse(color)
|
||||||
except ColorParseError:
|
except ColorParseError as error:
|
||||||
raise StyleValueError(
|
raise StyleValueError(
|
||||||
f"Invalid color value '{color}'",
|
f"Invalid color value '{color}'",
|
||||||
help_text=color_property_help_text(self.name, context="inline"),
|
help_text=color_property_help_text(
|
||||||
|
self.name, context="inline", error=error
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if obj.set_rule(self.name, parsed_color):
|
if obj.set_rule(self.name, parsed_color):
|
||||||
obj.refresh()
|
obj.refresh()
|
||||||
|
|||||||
@@ -572,9 +572,11 @@ class StylesBuilder:
|
|||||||
elif token.name in ("color", "token"):
|
elif token.name in ("color", "token"):
|
||||||
try:
|
try:
|
||||||
color = Color.parse(token.value)
|
color = Color.parse(token.value)
|
||||||
except Exception:
|
except Exception as error:
|
||||||
self.error(
|
self.error(
|
||||||
name, token, color_property_help_text(name, context="css")
|
name,
|
||||||
|
token,
|
||||||
|
color_property_help_text(name, context="css", error=error),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.error(name, token, color_property_help_text(name, context="css"))
|
self.error(name, token, color_property_help_text(name, context="css"))
|
||||||
|
|||||||
@@ -90,14 +90,48 @@ def test_did_you_mean_for_css_property_names(
|
|||||||
|
|
||||||
_, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
|
_, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
|
||||||
displayed_css_property_name = css_property_name.replace("_", "-")
|
displayed_css_property_name = css_property_name.replace("_", "-")
|
||||||
assert (
|
expected_summary = f"Invalid CSS property [i]{displayed_css_property_name}[/]"
|
||||||
help_text.summary == f"Invalid CSS property [i]{displayed_css_property_name}[/]"
|
if expected_property_name_suggestion:
|
||||||
|
expected_summary += f'. Did you mean "{expected_property_name_suggestion}"?'
|
||||||
|
assert help_text.summary == expected_summary
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"css_property_name,css_property_value,expected_color_suggestion",
|
||||||
|
[
|
||||||
|
["color", "blu", "blue"],
|
||||||
|
["background", "chartruse", "chartreuse"],
|
||||||
|
["tint", "ansi_whi", "ansi_white"],
|
||||||
|
["scrollbar-color", "transprnt", "transparent"],
|
||||||
|
["color", "xkcd", None],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_did_you_mean_for_color_names(
|
||||||
|
css_property_name: str, css_property_value: str, expected_color_suggestion
|
||||||
|
):
|
||||||
|
stylesheet = Stylesheet()
|
||||||
|
css = """
|
||||||
|
* {
|
||||||
|
border: blue;
|
||||||
|
${PROPERTY}: ${VALUE};
|
||||||
|
}
|
||||||
|
""".replace(
|
||||||
|
"${PROPERTY}", css_property_name
|
||||||
|
).replace(
|
||||||
|
"${VALUE}", css_property_value
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_bullets_length = 1 if expected_property_name_suggestion else 0
|
stylesheet.add_source(css)
|
||||||
assert len(help_text.bullets) == expected_bullets_length
|
with pytest.raises(StylesheetParseError) as err:
|
||||||
if expected_property_name_suggestion is not None:
|
stylesheet.parse()
|
||||||
expected_suggestion_message = (
|
|
||||||
f'Did you mean "{expected_property_name_suggestion}"?'
|
_, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
|
||||||
)
|
displayed_css_property_name = css_property_name.replace("_", "-")
|
||||||
assert help_text.bullets[0].markup == expected_suggestion_message
|
expected_error_summary = (
|
||||||
|
f"Invalid value for the [i]{displayed_css_property_name}[/] property"
|
||||||
|
)
|
||||||
|
|
||||||
|
if expected_color_suggestion is not None:
|
||||||
|
expected_error_summary += f'. Did you mean "{expected_color_suggestion}"?'
|
||||||
|
|
||||||
|
assert help_text.summary == expected_error_summary
|
||||||
|
|||||||
Reference in New Issue
Block a user