mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
[css] Address "did you mean" PR feedback
This commit is contained in:
@@ -149,7 +149,7 @@ def property_invalid_value_help_text(
|
|||||||
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 [i]{suggested_property_name}[/]?"))
|
bullets.append(Bullet(f'Did you mean "{suggested_property_name}"?'))
|
||||||
return HelpText(f"Invalid CSS property [i]{property_name}[/]", bullets=bullets)
|
return HelpText(f"Invalid CSS property [i]{property_name}[/]", bullets=bullets)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from difflib import get_close_matches
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import cast, Iterable, NoReturn
|
from typing import cast, Iterable, NoReturn, Sequence
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
@@ -47,6 +46,7 @@ from ..color import Color, ColorParseError
|
|||||||
from .._duration import _duration_as_seconds
|
from .._duration import _duration_as_seconds
|
||||||
from .._easing import EASING
|
from .._easing import EASING
|
||||||
from ..geometry import Spacing, SpacingDimensions, clamp
|
from ..geometry import Spacing, SpacingDimensions, clamp
|
||||||
|
from ..suggestions import get_suggestion
|
||||||
|
|
||||||
|
|
||||||
def _join_tokens(tokens: Iterable[Token], joiner: str = "") -> str:
|
def _join_tokens(tokens: Iterable[Token], joiner: str = "") -> str:
|
||||||
@@ -87,7 +87,7 @@ 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:
|
||||||
suggested_property_name = self._suggested_property_name_for_rule(
|
suggested_property_name = self._get_suggested_property_name_for_rule(
|
||||||
declaration.name
|
declaration.name
|
||||||
)
|
)
|
||||||
self.error(
|
self.error(
|
||||||
@@ -115,12 +115,14 @@ class StylesBuilder:
|
|||||||
self.error(declaration.name, declaration.token, str(error))
|
self.error(declaration.name, declaration.token, str(error))
|
||||||
|
|
||||||
@lru_cache(maxsize=None)
|
@lru_cache(maxsize=None)
|
||||||
def _processable_rule_names(self) -> frozenset[str]:
|
def _get_processable_rule_names(self) -> Sequence[str]:
|
||||||
return frozenset(
|
"""
|
||||||
[attr[8:] for attr in dir(self) if attr.startswith("process_")]
|
Returns the list of CSS properties we can manage -
|
||||||
)
|
i.e. the ones for which we have a `process_[property name]` method
|
||||||
|
"""
|
||||||
|
return [attr[8:] for attr in dir(self) if attr.startswith("process_")]
|
||||||
|
|
||||||
def _process_enum_multiple(
|
def _get_process_enum_multiple(
|
||||||
self, name: str, tokens: list[Token], valid_values: set[str], count: int
|
self, name: str, tokens: list[Token], valid_values: set[str], count: int
|
||||||
) -> tuple[str, ...]:
|
) -> tuple[str, ...]:
|
||||||
"""Generic code to process a declaration with two enumerations, like overflow: auto auto"""
|
"""Generic code to process a declaration with two enumerations, like overflow: auto auto"""
|
||||||
@@ -745,8 +747,10 @@ 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 _suggested_property_name_for_rule(self, rule_name: str) -> str | None:
|
def _get_suggested_property_name_for_rule(self, rule_name: str) -> str | None:
|
||||||
possible_matches = get_close_matches(
|
"""
|
||||||
rule_name, self._processable_rule_names(), n=1
|
Returns a valid CSS property "Python" name, or None if no close matches could be found.
|
||||||
)
|
|
||||||
return None if not possible_matches else possible_matches[0]
|
Example: returns "background" for rule_name "bkgrund", "offset_x" for "ofset_x"
|
||||||
|
"""
|
||||||
|
return get_suggestion(rule_name, self._get_processable_rule_names())
|
||||||
|
|||||||
24
src/textual/suggestions.py
Normal file
24
src/textual/suggestions.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from difflib import get_close_matches
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
|
||||||
|
def get_suggestion(word: str, possible_words: Sequence[str]) -> str | None:
|
||||||
|
"""
|
||||||
|
Returns a close match of 'word' amongst 'possible_words', or None if no close matches could be found.
|
||||||
|
|
||||||
|
Example: returns "red" for word "redu" and possible words ("yellow", "red")
|
||||||
|
"""
|
||||||
|
possible_matches = get_close_matches(word, possible_words, n=1)
|
||||||
|
return None if not possible_matches else possible_matches[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_suggestions(word: str, possible_words: Sequence[str], count: int) -> list[str]:
|
||||||
|
"""
|
||||||
|
Returns a list of up to 'count' matches of 'word' amongst 'possible_words' -
|
||||||
|
or an empty list if no close matches could be found.
|
||||||
|
|
||||||
|
Example: returns ["yellow", "ellow"] for word "yllow" and possible words ("yellow", "red", "ellow")
|
||||||
|
"""
|
||||||
|
return get_close_matches(word, possible_words, n=count)
|
||||||
@@ -64,6 +64,7 @@ def test_color_property_parsing(css_value, expectation, expected_color):
|
|||||||
["backgroundu", "background"],
|
["backgroundu", "background"],
|
||||||
["bckgroundu", "background"],
|
["bckgroundu", "background"],
|
||||||
["ofset-x", "offset-x"],
|
["ofset-x", "offset-x"],
|
||||||
|
["ofst_y", "offset-y"],
|
||||||
["colr", "color"],
|
["colr", "color"],
|
||||||
["colour", "color"],
|
["colour", "color"],
|
||||||
["wdth", "width"],
|
["wdth", "width"],
|
||||||
@@ -73,7 +74,7 @@ def test_color_property_parsing(css_value, expectation, expected_color):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_did_you_mean_for_css_property_names(
|
def test_did_you_mean_for_css_property_names(
|
||||||
css_property_name, expected_property_name_suggestion
|
css_property_name: str, expected_property_name_suggestion
|
||||||
):
|
):
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
css = """
|
css = """
|
||||||
@@ -90,12 +91,15 @@ def test_did_you_mean_for_css_property_names(
|
|||||||
stylesheet.parse()
|
stylesheet.parse()
|
||||||
|
|
||||||
_, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
|
_, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
|
||||||
assert help_text.summary == f"Invalid CSS property [i]{css_property_name}[/]"
|
displayed_css_property_name = css_property_name.replace("_", "-")
|
||||||
|
assert (
|
||||||
|
help_text.summary == f"Invalid CSS property [i]{displayed_css_property_name}[/]"
|
||||||
|
)
|
||||||
|
|
||||||
expected_bullets_length = 1 if expected_property_name_suggestion else 0
|
expected_bullets_length = 1 if expected_property_name_suggestion else 0
|
||||||
assert len(help_text.bullets) == expected_bullets_length
|
assert len(help_text.bullets) == expected_bullets_length
|
||||||
if expected_property_name_suggestion is not None:
|
if expected_property_name_suggestion is not None:
|
||||||
expected_suggestion_message = (
|
expected_suggestion_message = (
|
||||||
f"Did you mean [i]{expected_property_name_suggestion}[/]?"
|
f'Did you mean "{expected_property_name_suggestion}"?'
|
||||||
)
|
)
|
||||||
assert help_text.bullets[0].markup == expected_suggestion_message
|
assert help_text.bullets[0].markup == expected_suggestion_message
|
||||||
|
|||||||
35
tests/test_suggestions.py
Normal file
35
tests/test_suggestions.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from textual.suggestions import get_suggestion, get_suggestions
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"word, possible_words, expected_result",
|
||||||
|
(
|
||||||
|
["background", ("background",), "background"],
|
||||||
|
["backgroundu", ("background",), "background"],
|
||||||
|
["bkgrund", ("background",), "background"],
|
||||||
|
["llow", ("background",), None],
|
||||||
|
["llow", ("background", "yellow"), "yellow"],
|
||||||
|
["yllow", ("background", "yellow", "ellow"), "yellow"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_get_suggestion(word, possible_words, expected_result):
|
||||||
|
assert get_suggestion(word, possible_words) == expected_result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"word, possible_words, count, expected_result",
|
||||||
|
(
|
||||||
|
["background", ("background",), 1, ["background"]],
|
||||||
|
["backgroundu", ("background",), 1, ["background"]],
|
||||||
|
["bkgrund", ("background",), 1, ["background"]],
|
||||||
|
["llow", ("background",), 1, []],
|
||||||
|
["llow", ("background", "yellow"), 1, ["yellow"]],
|
||||||
|
["yllow", ("background", "yellow", "ellow"), 1, ["yellow"]],
|
||||||
|
["yllow", ("background", "yellow", "ellow"), 2, ["yellow", "ellow"]],
|
||||||
|
["yllow", ("background", "yellow", "red"), 2, ["yellow"]],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_get_suggestions(word, possible_words, count, expected_result):
|
||||||
|
assert get_suggestions(word, possible_words, count) == expected_result
|
||||||
Reference in New Issue
Block a user