diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py index 6dac2bee2..561f3e026 100644 --- a/src/textual/css/_help_text.py +++ b/src/textual/css/_help_text.py @@ -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, num_values_supplied: int, 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, context: StylingContext, ) -> HelpText: diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 01b64b001..ecafefa86 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -22,7 +22,7 @@ from ._help_text import ( style_flags_property_help_text, ) from ._help_text import ( - spacing_wrong_number_of_values, + spacing_wrong_number_of_values_help_text, scalar_help_text, string_enum_help_text, color_property_help_text, @@ -415,7 +415,7 @@ class SpacingProperty: except ValueError as error: raise StyleValueError( str(error), - help_text=spacing_wrong_number_of_values( + help_text=spacing_wrong_number_of_values_help_text( property_name=self.name, num_values_supplied=len(spacing), context="inline", diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 9d764a4d3..e981edd4b 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -9,8 +9,8 @@ import rich.repr from ._error_tools import friendly_list from ._help_renderables import HelpText from ._help_text import ( - spacing_invalid_value, - spacing_wrong_number_of_values, + spacing_invalid_value_help_text, + spacing_wrong_number_of_values_help_text, scalar_help_text, color_property_help_text, string_enum_help_text, @@ -23,6 +23,7 @@ from ._help_text import ( offset_property_help_text, offset_single_axis_help_text, style_flags_property_help_text, + property_invalid_value_help_text, ) from .constants import ( VALID_ALIGN_HORIZONTAL, @@ -86,14 +87,17 @@ class StylesBuilder: process_method = getattr(self, f"process_{rule_name}", None) if process_method is None: - error_message = f"unknown declaration {declaration.name!r}" - did_you_mean_rule_name = self._did_you_mean_for_rule_name(declaration.name) - if did_you_mean_rule_name: - error_message += f"; did you mean {did_you_mean_rule_name!r}?" + suggested_property_name = self._suggested_property_name_for_rule( + declaration.name + ) self.error( declaration.name, declaration.token, - error_message, + property_invalid_value_help_text( + declaration.name, + "css", + suggested_property_name=suggested_property_name, + ), ) return @@ -345,14 +349,20 @@ class StylesBuilder: try: append(int(value)) 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: - 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): self.error( name, tokens[0], - spacing_wrong_number_of_values( + spacing_wrong_number_of_values_help_text( 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: """Process granular margin / padding declarations.""" 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} token = tokens[0] @@ -369,7 +381,9 @@ class StylesBuilder: if token_name == "number": space = int(value) 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("_") current_spacing = cast( @@ -731,7 +745,7 @@ class StylesBuilder: process_content_align_horizontal = process_align_horizontal 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( rule_name, self._processable_rule_names(), n=1 ) diff --git a/tests/css/test_help_text.py b/tests/css/test_help_text.py index 5c34c3974..f5818a11d 100644 --- a/tests/css/test_help_text.py +++ b/tests/css/test_help_text.py @@ -1,10 +1,22 @@ import pytest from tests.utilities.render import render -from textual.css._help_text import spacing_wrong_number_of_values, spacing_invalid_value, 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 +from textual.css._help_text import ( + spacing_wrong_number_of_values_help_text, + spacing_invalid_value_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"]) @@ -15,22 +27,24 @@ def styling_context(request): def test_help_text_examples_are_contextualized(): """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.""" - 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 - rendered_css = render(spacing_invalid_value("padding", "css")) + rendered_css = render(spacing_invalid_value_help_text("padding", "css")) assert "padding:" in rendered_css 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 "margin" in rendered assert "3" in rendered 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 "padding" in rendered @@ -47,7 +61,9 @@ def test_scalar_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 # 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): - 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 "notavalue" in rendered diff --git a/tests/css/test_stylesheet.py b/tests/css/test_stylesheet.py index 5d4978405..fe9e36456 100644 --- a/tests/css/test_stylesheet.py +++ b/tests/css/test_stylesheet.py @@ -1,7 +1,10 @@ from contextlib import nullcontext as does_not_raise +from typing import Any + import pytest from textual.color import Color +from textual.css._help_renderables import HelpText from textual.css.stylesheet import Stylesheet, StylesheetParseError from textual.css.tokenizer import TokenizeError @@ -60,6 +63,7 @@ def test_color_property_parsing(css_value, expectation, expected_color): [ ["backgroundu", "background"], ["bckgroundu", "background"], + ["ofset-x", "offset-x"], ["colr", "color"], ["colour", "color"], ["wdth", "width"], @@ -81,12 +85,17 @@ def test_did_you_mean_for_css_property_names( "${PROPERTY}", css_property_name ) + stylesheet.add_source(css) with pytest.raises(StylesheetParseError) as err: - stylesheet.parse(css) + stylesheet.parse() - error_token, error_message = err.value.errors.stylesheet.rules[0].errors[0] - if expected_property_name_suggestion is None: - assert "did you mean" not in error_message - else: - expected_did_you_mean_error_message = f"unknown declaration '{css_property_name}'; did you mean '{expected_property_name_suggestion}'?" - assert expected_did_you_mean_error_message == error_message + _, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText + assert help_text.summary == f"Invalid CSS property [i]{css_property_name}[/]" + + expected_bullets_length = 1 if expected_property_name_suggestion else 0 + assert len(help_text.bullets) == expected_bullets_length + 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