[css] Use a HelpText for unknown CSS property name errors

This commit is contained in:
Olivier Philippon
2022-04-29 12:24:06 +01:00
parent 2c03f8cfe1
commit f5aac5d028
5 changed files with 100 additions and 34 deletions

View File

@@ -128,7 +128,32 @@ def _spacing_examples(property_name: str) -> ContextSpecificBullets:
) )
def spacing_wrong_number_of_values( def property_invalid_value_help_text(
property_name: str, context: StylingContext, *, suggested_property_name: str = None
) -> HelpText:
"""Help text to show when the user supplies an invalid value for CSS property
property.
Args:
property_name (str): The name of the property
context (StylingContext | None): The context the spacing property is being used in.
Keyword Args:
suggested_property_name (str | None): A suggested name for the property (e.g. "width" for "wdth"). Defaults to None.
Returns:
HelpText: Renderable for displaying the help text for this property
"""
property_name = _contextualize_property_name(property_name, context)
bullets = []
if suggested_property_name:
suggested_property_name = _contextualize_property_name(
suggested_property_name, context
)
bullets.append(Bullet(f"Did you mean [i]{suggested_property_name}[/]?"))
return HelpText(f"Invalid CSS property [i]{property_name}[/]", bullets=bullets)
def spacing_wrong_number_of_values_help_text(
property_name: str, property_name: str,
num_values_supplied: int, num_values_supplied: int,
context: StylingContext, context: StylingContext,
@@ -159,7 +184,7 @@ def spacing_wrong_number_of_values(
) )
def spacing_invalid_value( def spacing_invalid_value_help_text(
property_name: str, property_name: str,
context: StylingContext, context: StylingContext,
) -> HelpText: ) -> HelpText:

View File

@@ -22,7 +22,7 @@ from ._help_text import (
style_flags_property_help_text, style_flags_property_help_text,
) )
from ._help_text import ( from ._help_text import (
spacing_wrong_number_of_values, spacing_wrong_number_of_values_help_text,
scalar_help_text, scalar_help_text,
string_enum_help_text, string_enum_help_text,
color_property_help_text, color_property_help_text,
@@ -415,7 +415,7 @@ class SpacingProperty:
except ValueError as error: except ValueError as error:
raise StyleValueError( raise StyleValueError(
str(error), str(error),
help_text=spacing_wrong_number_of_values( help_text=spacing_wrong_number_of_values_help_text(
property_name=self.name, property_name=self.name,
num_values_supplied=len(spacing), num_values_supplied=len(spacing),
context="inline", context="inline",

View File

@@ -9,8 +9,8 @@ import rich.repr
from ._error_tools import friendly_list from ._error_tools import friendly_list
from ._help_renderables import HelpText from ._help_renderables import HelpText
from ._help_text import ( from ._help_text import (
spacing_invalid_value, spacing_invalid_value_help_text,
spacing_wrong_number_of_values, spacing_wrong_number_of_values_help_text,
scalar_help_text, scalar_help_text,
color_property_help_text, color_property_help_text,
string_enum_help_text, string_enum_help_text,
@@ -23,6 +23,7 @@ from ._help_text import (
offset_property_help_text, offset_property_help_text,
offset_single_axis_help_text, offset_single_axis_help_text,
style_flags_property_help_text, style_flags_property_help_text,
property_invalid_value_help_text,
) )
from .constants import ( from .constants import (
VALID_ALIGN_HORIZONTAL, VALID_ALIGN_HORIZONTAL,
@@ -86,14 +87,17 @@ class StylesBuilder:
process_method = getattr(self, f"process_{rule_name}", None) process_method = getattr(self, f"process_{rule_name}", None)
if process_method is None: if process_method is None:
error_message = f"unknown declaration {declaration.name!r}" suggested_property_name = self._suggested_property_name_for_rule(
did_you_mean_rule_name = self._did_you_mean_for_rule_name(declaration.name) declaration.name
if did_you_mean_rule_name: )
error_message += f"; did you mean {did_you_mean_rule_name!r}?"
self.error( self.error(
declaration.name, declaration.name,
declaration.token, declaration.token,
error_message, property_invalid_value_help_text(
declaration.name,
"css",
suggested_property_name=suggested_property_name,
),
) )
return return
@@ -345,14 +349,20 @@ class StylesBuilder:
try: try:
append(int(value)) append(int(value))
except ValueError: except ValueError:
self.error(name, token, spacing_invalid_value(name, context="css")) self.error(
name,
token,
spacing_invalid_value_help_text(name, context="css"),
)
else: else:
self.error(name, token, spacing_invalid_value(name, context="css")) self.error(
name, token, spacing_invalid_value_help_text(name, context="css")
)
if len(space) not in (1, 2, 4): if len(space) not in (1, 2, 4):
self.error( self.error(
name, name,
tokens[0], tokens[0],
spacing_wrong_number_of_values( spacing_wrong_number_of_values_help_text(
name, num_values_supplied=len(space), context="css" name, num_values_supplied=len(space), context="css"
), ),
) )
@@ -361,7 +371,9 @@ class StylesBuilder:
def _process_space_partial(self, name: str, tokens: list[Token]) -> None: def _process_space_partial(self, name: str, tokens: list[Token]) -> None:
"""Process granular margin / padding declarations.""" """Process granular margin / padding declarations."""
if len(tokens) != 1: if len(tokens) != 1:
self.error(name, tokens[0], spacing_invalid_value(name, context="css")) self.error(
name, tokens[0], spacing_invalid_value_help_text(name, context="css")
)
_EDGE_SPACING_MAP = {"top": 0, "right": 1, "bottom": 2, "left": 3} _EDGE_SPACING_MAP = {"top": 0, "right": 1, "bottom": 2, "left": 3}
token = tokens[0] token = tokens[0]
@@ -369,7 +381,9 @@ class StylesBuilder:
if token_name == "number": if token_name == "number":
space = int(value) space = int(value)
else: else:
self.error(name, token, spacing_invalid_value(name, context="css")) self.error(
name, token, spacing_invalid_value_help_text(name, context="css")
)
style_name, _, edge = name.replace("-", "_").partition("_") style_name, _, edge = name.replace("-", "_").partition("_")
current_spacing = cast( current_spacing = cast(
@@ -731,7 +745,7 @@ class StylesBuilder:
process_content_align_horizontal = process_align_horizontal process_content_align_horizontal = process_align_horizontal
process_content_align_vertical = process_align_vertical process_content_align_vertical = process_align_vertical
def _did_you_mean_for_rule_name(self, rule_name: str) -> str | None: def _suggested_property_name_for_rule(self, rule_name: str) -> str | None:
possible_matches = get_close_matches( possible_matches = get_close_matches(
rule_name, self._processable_rule_names(), n=1 rule_name, self._processable_rule_names(), n=1
) )

View File

@@ -1,10 +1,22 @@
import pytest import pytest
from tests.utilities.render import render from tests.utilities.render import render
from textual.css._help_text import spacing_wrong_number_of_values, spacing_invalid_value, scalar_help_text, \ from textual.css._help_text import (
string_enum_help_text, color_property_help_text, border_property_help_text, layout_property_help_text, \ spacing_wrong_number_of_values_help_text,
docks_property_help_text, dock_property_help_text, fractional_property_help_text, offset_property_help_text, \ spacing_invalid_value_help_text,
align_help_text, offset_single_axis_help_text, style_flags_property_help_text scalar_help_text,
string_enum_help_text,
color_property_help_text,
border_property_help_text,
layout_property_help_text,
docks_property_help_text,
dock_property_help_text,
fractional_property_help_text,
offset_property_help_text,
align_help_text,
offset_single_axis_help_text,
style_flags_property_help_text,
)
@pytest.fixture(params=["css", "inline"]) @pytest.fixture(params=["css", "inline"])
@@ -15,22 +27,24 @@ def styling_context(request):
def test_help_text_examples_are_contextualized(): def test_help_text_examples_are_contextualized():
"""Ensure that if the user is using CSS, they see CSS-specific examples """Ensure that if the user is using CSS, they see CSS-specific examples
and if they're using inline styles they see inline-specific examples.""" and if they're using inline styles they see inline-specific examples."""
rendered_inline = render(spacing_invalid_value("padding", "inline")) rendered_inline = render(spacing_invalid_value_help_text("padding", "inline"))
assert "widget.styles.padding" in rendered_inline assert "widget.styles.padding" in rendered_inline
rendered_css = render(spacing_invalid_value("padding", "css")) rendered_css = render(spacing_invalid_value_help_text("padding", "css"))
assert "padding:" in rendered_css assert "padding:" in rendered_css
def test_spacing_wrong_number_of_values(styling_context): def test_spacing_wrong_number_of_values(styling_context):
rendered = render(spacing_wrong_number_of_values("margin", 3, styling_context)) rendered = render(
spacing_wrong_number_of_values_help_text("margin", 3, styling_context)
)
assert "Invalid number of values" in rendered assert "Invalid number of values" in rendered
assert "margin" in rendered assert "margin" in rendered
assert "3" in rendered assert "3" in rendered
def test_spacing_invalid_value(styling_context): def test_spacing_invalid_value(styling_context):
rendered = render(spacing_invalid_value("padding", styling_context)) rendered = render(spacing_invalid_value_help_text("padding", styling_context))
assert "Invalid value for" in rendered assert "Invalid value for" in rendered
assert "padding" in rendered assert "padding" in rendered
@@ -47,7 +61,9 @@ def test_scalar_help_text(styling_context):
def test_string_enum_help_text(styling_context): def test_string_enum_help_text(styling_context):
rendered = render(string_enum_help_text("display", ["none", "hidden"], styling_context)) rendered = render(
string_enum_help_text("display", ["none", "hidden"], styling_context)
)
assert "Invalid value for" in rendered assert "Invalid value for" in rendered
# Ensure property name is mentioned # Ensure property name is mentioned
@@ -113,7 +129,9 @@ def test_offset_single_axis_help_text():
def test_style_flags_property_help_text(styling_context): def test_style_flags_property_help_text(styling_context):
rendered = render(style_flags_property_help_text("text-style", "notavalue b", styling_context)) rendered = render(
style_flags_property_help_text("text-style", "notavalue b", styling_context)
)
assert "Invalid value" in rendered assert "Invalid value" in rendered
assert "notavalue" in rendered assert "notavalue" in rendered

View File

@@ -1,7 +1,10 @@
from contextlib import nullcontext as does_not_raise from contextlib import nullcontext as does_not_raise
from typing import Any
import pytest import pytest
from textual.color import Color from textual.color import Color
from textual.css._help_renderables import HelpText
from textual.css.stylesheet import Stylesheet, StylesheetParseError from textual.css.stylesheet import Stylesheet, StylesheetParseError
from textual.css.tokenizer import TokenizeError from textual.css.tokenizer import TokenizeError
@@ -60,6 +63,7 @@ def test_color_property_parsing(css_value, expectation, expected_color):
[ [
["backgroundu", "background"], ["backgroundu", "background"],
["bckgroundu", "background"], ["bckgroundu", "background"],
["ofset-x", "offset-x"],
["colr", "color"], ["colr", "color"],
["colour", "color"], ["colour", "color"],
["wdth", "width"], ["wdth", "width"],
@@ -81,12 +85,17 @@ def test_did_you_mean_for_css_property_names(
"${PROPERTY}", css_property_name "${PROPERTY}", css_property_name
) )
stylesheet.add_source(css)
with pytest.raises(StylesheetParseError) as err: with pytest.raises(StylesheetParseError) as err:
stylesheet.parse(css) stylesheet.parse()
error_token, error_message = err.value.errors.stylesheet.rules[0].errors[0] _, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
if expected_property_name_suggestion is None: assert help_text.summary == f"Invalid CSS property [i]{css_property_name}[/]"
assert "did you mean" not in error_message
else: expected_bullets_length = 1 if expected_property_name_suggestion else 0
expected_did_you_mean_error_message = f"unknown declaration '{css_property_name}'; did you mean '{expected_property_name_suggestion}'?" assert len(help_text.bullets) == expected_bullets_length
assert expected_did_you_mean_error_message == error_message if expected_property_name_suggestion is not None:
expected_suggestion_message = (
f"Did you mean [i]{expected_property_name_suggestion}[/]?"
)
assert help_text.bullets[0].markup == expected_suggestion_message