test fixes

This commit is contained in:
Will McGugan
2025-02-03 15:51:41 +00:00
parent 649abb5d18
commit bba0b4f466
15 changed files with 142 additions and 36 deletions

View File

@@ -39,7 +39,7 @@ class BorderSubTitleAlignAll(App[None]):
"had to fill up",
"lbl4",
"", # (4)!
"[link=https://textual.textualize.io]Left[/]", # (5)!
"[link='https://textual.textualize.io']Left[/]", # (5)!
)
yield make_label_container( # (6)!
"nine labels", "lbl5", "Title", "Subtitle"

View File

@@ -7,7 +7,7 @@ class LinkBackgroundApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(

View File

@@ -7,7 +7,7 @@ class LinkHoverBackgroundApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(

View File

@@ -7,7 +7,7 @@ class LinkColorApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(

View File

@@ -7,7 +7,7 @@ class LinkHoverColorApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(

View File

@@ -7,7 +7,7 @@ class LinkStyleApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(

View File

@@ -7,7 +7,7 @@ class LinkHoverStyleApp(App):
def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(

View File

@@ -878,7 +878,7 @@ class Content(Visual):
"""Render Content in to an iterable of strings and styles.
This is typically called by Textual when displaying Content, but may be used if you want to do more advanced
pricessing of the output.
processing of the output.
Args:
base_style (_type_, optional): The style used as a base. This will typically be the style of the widget underneath the content.

View File

@@ -405,6 +405,7 @@ def substitute_references(
)
)
else:
1 / 0
_unresolved(ref_name, variables.keys(), token)
else:
variable_tokens.append(token)
@@ -422,6 +423,7 @@ def substitute_references(
ReferencedBy(variable_name, ref_location, ref_length, ref_code)
)
else:
1 / 0
_unresolved(variable_name, variables.keys(), token)
else:
yield token

View File

@@ -231,10 +231,7 @@ class Stylesheet:
"""
if style_text in self._style_parse_cache:
return self._style_parse_cache[style_text]
try:
style = parse_style(style_text)
except Exception:
style = Style.null()
style = parse_style(style_text)
self._style_parse_cache[style_text] = style
return style

View File

@@ -160,6 +160,15 @@ def escape(
def parse_style(style: str, variables: dict[str, str] | None = None) -> Style:
"""Parse an encoded style.
Args:
style: Style encoded in a string.
variables: Mapping of variables, or `None` to import from active app.
Returns:
A Style object.
"""
styles: dict[str, bool | None] = {}
color: Color | None = None
@@ -222,7 +231,10 @@ def parse_style(style: str, variables: dict[str, str] | None = None) -> Style:
color = Color.parse(token.value)
elif token_name == "token":
if token_value == "on":
if token_value == "link":
if "link" not in meta:
meta["link"] = ""
elif token_value == "on":
is_background = True
elif token_value == "auto":
if is_background:
@@ -252,7 +264,7 @@ def parse_style(style: str, variables: dict[str, str] | None = None) -> Style:
if color is not None:
color = color.multiply_alpha(percent)
parsed_style = Style(background, color, link=meta.get("link", None), **styles)
parsed_style = Style(background, color, link=meta.pop("link", None), **styles)
if meta:
parsed_style += Style.from_meta(meta)
@@ -260,14 +272,39 @@ def parse_style(style: str, variables: dict[str, str] | None = None) -> Style:
def to_content(markup: str, style: str | Style = "") -> Content:
"""Convert markup to Content.
Args:
markup: String containing markup.
style: Optional base style.
Raises:
MarkupError: If the markup is invalid.
Returns:
Content that renders the markup.
"""
_rich_traceback_omit = True
try:
return _to_content(markup, style)
except Exception as error:
# Ensure all errors are wrapped in a MarkupError
raise MarkupError(str(error)) from None
def _to_content(markup: str, style: str | Style = "") -> Content:
"""Internal function to convert markup to Content.
Args:
markup: String containing markup.
style: Optional base style.
Raises:
MarkupError: If the markup is invalid.
Returns:
Content that renders the markup.
"""
from textual.content import Content, Span
@@ -275,12 +312,15 @@ def _to_content(markup: str, style: str | Style = "") -> Content:
text: list[str] = []
iter_tokens = iter(tokenizer(markup, ("inline", "")))
style_stack: list[tuple[int, str]] = []
style_stack: list[tuple[int, str, str]] = []
spans: list[Span] = []
position = 0
tag_text: list[str]
normalize_markup_tag = Style._normalize_markup_tag
for token in iter_tokens:
token_name = token.name
@@ -295,7 +335,9 @@ def _to_content(markup: str, style: str | Style = "") -> Content:
break
tag_text.append(token.value)
opening_tag = "".join(tag_text).strip()
style_stack.append((position, opening_tag))
style_stack.append(
(position, opening_tag, normalize_markup_tag(opening_tag))
)
elif token_name == "open_closing_tag":
tag_text = []
@@ -304,30 +346,32 @@ def _to_content(markup: str, style: str | Style = "") -> Content:
break
tag_text.append(token.value)
closing_tag = "".join(tag_text).strip()
if closing_tag:
for index, (tag_position, tag_body) in enumerate(
normalized_closing_tag = normalize_markup_tag(closing_tag)
if normalized_closing_tag:
for index, (tag_position, tag_body, normalized_tag_body) in enumerate(
reversed(style_stack), 1
):
if tag_body == closing_tag:
if normalized_tag_body == normalized_closing_tag:
style_stack.pop(-index)
spans.append(Span(tag_position, position, tag_body))
if tag_position != position:
spans.append(Span(tag_position, position, tag_body))
break
else:
raise MarkupError(
f"closing tag '[/{tag_body}]' does not match any open tag"
f"closing tag '[/{closing_tag}]' does not match any open tag"
)
else:
if not style_stack:
raise MarkupError("auto closing tag ('[/]') has nothing to close")
open_position, tag = style_stack.pop()
spans.append(Span(open_position, position, tag))
open_position, tag_body, _ = style_stack.pop()
spans.append(Span(open_position, position, tag_body))
content_text = "".join(text)
text_length = len(content_text)
while style_stack:
position, tag = style_stack.pop()
spans.append(Span(position, text_length, tag))
position, tag_body, _ = style_stack.pop()
spans.append(Span(position, text_length, tag_body))
if style:
content = Content(content_text, [Span(0, len(content_text), style), *spans])

View File

@@ -103,21 +103,71 @@ class Style:
return not self._is_null
def __str__(self) -> str:
return self.style_definition
@cached_property
def style_definition(self) -> str:
"""Style encoded in a string (may be parsed from `Style.parse`)."""
output: list[str] = []
output_append = output.append
if self.foreground is not None:
output.append(self.foreground.css)
output_append(self.foreground.css)
if self.background is not None:
output.append(f"on {self.background.css}")
output_append(f"on {self.background.css}")
if self.bold is not None:
output.append("bold" if self.bold else "not bold")
output_append("bold" if self.bold else "not bold")
if self.dim is not None:
output.append("dim" if self.dim else "not dim")
output_append("dim" if self.dim else "not dim")
if self.italic is not None:
output.append("italic" if self.italic else "not italic")
output_append("italic" if self.italic else "not italic")
if self.underline is not None:
output.append("underline" if self.underline else "not underline")
output_append("underline" if self.underline else "not underline")
if self.strike is not None:
output.append("strike" if self.strike else "not strike")
output_append("strike" if self.strike else "not strike")
if self.link is not None:
if "'" not in self.link:
output_append(f"link='{self.link}'")
elif '"' not in self.link:
output_append(f'link="{self.link}"')
if self._meta is not None:
for key, value in self.meta.items():
if isinstance(value, str):
if "'" not in key:
output_append(f"{key}='{value}'")
elif '"' not in key:
output_append(f'{key}="{value}"')
else:
output_append(f"{key}={value!r}")
else:
output_append(f"{key}={value!r}")
return " ".join(output)
@cached_property
def markup_tag(self) -> str:
"""Identifier used to close tags in markup."""
output: list[str] = []
output_append = output.append
if self.foreground is not None:
output_append(self.foreground.css)
if self.background is not None:
output_append(f"on {self.background.css}")
if self.bold is not None:
output_append("bold" if self.bold else "not bold")
if self.dim is not None:
output_append("dim" if self.dim else "not dim")
if self.italic is not None:
output_append("italic" if self.italic else "not italic")
if self.underline is not None:
output_append("underline" if self.underline else "not underline")
if self.strike is not None:
output_append("strike" if self.strike else "not strike")
if self.link is not None:
output_append("link")
if self._meta is not None:
for key, value in self.meta.items():
if isinstance(value, str):
output_append(f"{key}=")
return " ".join(output)
@@ -176,6 +226,22 @@ class Style:
return parse_style(text_style, variables)
return app.stylesheet.parse_style(text_style)
@classmethod
def _normalize_markup_tag(cls, text_style: str) -> str:
"""Produces a normalized from of a style, used to match closing tags with opening tags.
Args:
text_style: Style to normalize.
Returns:
Normalized markup tag.
"""
try:
style = cls.parse(text_style)
except Exception:
return text_style.strip()
return style.markup_tag
@classmethod
def from_rich_style(
cls, rich_style: RichStyle, theme: TerminalTheme | None = None

View File

@@ -69,6 +69,7 @@ def visualize(widget: Widget, obj: object, markup: bool = True) -> Visual:
Returns:
A Visual instance to render the object, or `None` if there is no associated visual.
"""
_rich_traceback_omit = True
if isinstance(obj, Visual):
# Already a visual
return obj

View File

@@ -3869,7 +3869,6 @@ class Widget(DOMNode):
self._render_content()
try:
line = self._render_cache.lines[y]
print(repr(line))
except IndexError:
line = Strip.blank(self.size.width, self.rich_style)

View File

@@ -11,7 +11,6 @@ async def test_issue_4248() -> None:
class ActionApp(App[None]):
def compose(self) -> ComposeResult:
yield Label("[@click]click me and crash[/]", id="nothing")
yield Label("[@click=]click me and crash[/]", id="no-params")
yield Label("[@click=()]click me and crash[/]", id="empty-params")
yield Label("[@click=foobar]click me[/]", id="unknown-sans-parens")
@@ -26,8 +25,6 @@ async def test_issue_4248() -> None:
app = ActionApp()
async with app.run_test() as pilot:
assert bumps == 0
await pilot.click("#nothing")
assert bumps == 0
await pilot.click("#no-params")
assert bumps == 0