mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
test fixes
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user