Back to Repositories

Validating Rich Markup Processing and Style Rendering in Textualize/rich

This test suite validates the markup functionality in Rich, focusing on tag parsing, escaping, and rendering capabilities. It ensures proper handling of Rich’s markup syntax for text styling and formatting.

Test Coverage Overview

The test suite provides comprehensive coverage of Rich’s markup system, including:
  • Regular expression pattern matching for valid/invalid tags
  • Tag escaping mechanisms and backslash handling
  • Markup parsing and rendering functionality
  • Link tag processing
  • Style combination and overlap handling
  • Event handling and meta-data attachment

Implementation Analysis

The testing approach employs pytest fixtures and assertions to verify markup behavior. Tests utilize pattern matching, string manipulation, and Rich’s Text object model to validate markup processing. Key patterns include systematic validation of escape sequences and style inheritance.

Technical Details

Testing tools and components include:
  • pytest framework for test organization
  • Rich’s Console class for output capture
  • Text and Span classes for style verification
  • Regular expressions for tag validation
  • Custom MarkupError handling

Best Practices Demonstrated

The test suite exemplifies strong testing practices including:
  • Isolated unit tests for specific functionality
  • Edge case coverage for escape sequences
  • Comprehensive validation of error conditions
  • Clear test naming and organization
  • Systematic verification of style application

textualize/rich

tests/test_markup.py

            
import pytest

from rich.console import Console
from rich.errors import MarkupError
from rich.markup import RE_TAGS, Tag, _parse, escape, render
from rich.text import Span, Text


def test_re_no_match():
    assert RE_TAGS.match("[True]") == None
    assert RE_TAGS.match("[False]") == None
    assert RE_TAGS.match("[None]") == None
    assert RE_TAGS.match("[1]") == None
    assert RE_TAGS.match("[2]") == None
    assert RE_TAGS.match("[]") == None


def test_re_match():
    assert RE_TAGS.match("[true]")
    assert RE_TAGS.match("[false]")
    assert RE_TAGS.match("[none]")
    assert RE_TAGS.match("[color(1)]")
    assert RE_TAGS.match("[#ff00ff]")
    assert RE_TAGS.match("[/]")
    assert RE_TAGS.match("[@]")
    assert RE_TAGS.match("[@foo]")
    assert RE_TAGS.match("[@foo=bar]")


def test_escape():
    # Potential tags
    assert escape("foo[bar]") == r"foo\[bar]"
    assert escape(r"foo\[bar]") == r"foo\\\[bar]"

    # Not tags (escape not required)
    assert escape("[5]") == "[5]"
    assert escape("\\[5]") == "\\[5]"

    # Test @ escape
    assert escape("[@foo]") == "\\[@foo]"
    assert escape("[@]") == "\\[@]"

    # https://github.com/Textualize/rich/issues/2187
    assert escape("[nil, [nil]]") == r"[nil, \[nil]]"


def test_escape_backslash_end():
    # https://github.com/Textualize/rich/issues/2987
    value = "C:\\"
    assert escape(value) == "C:\\\\"

    escaped_tags = f"[red]{escape(value)}[/red]"
    assert escaped_tags == "[red]C:\\\\[/red]"
    escaped_text = Text.from_markup(escaped_tags)
    assert escaped_text.plain == "C:\\"
    assert escaped_text.spans == [Span(0, 3, "red")]


def test_render_escape():
    console = Console(width=80, color_system=None)
    console.begin_capture()
    console.print(
        escape(r"[red]"), escape(r"\[red]"), escape(r"\\[red]"), escape(r"\\\[red]")
    )
    result = console.end_capture()
    expected = r"[red] \[red] \\[red] \\\[red]" + "
"
    assert result == expected


def test_parse():
    result = list(_parse(r"[foo]hello[/foo][bar]world[/]\[escaped]"))
    expected = [
        (0, None, Tag(name="foo", parameters=None)),
        (10, "hello", None),
        (10, None, Tag(name="/foo", parameters=None)),
        (16, None, Tag(name="bar", parameters=None)),
        (26, "world", None),
        (26, None, Tag(name="/", parameters=None)),
        (29, "[escaped]", None),
    ]
    print(repr(result))
    assert result == expected


def test_parse_link():
    result = list(_parse("[link=foo]bar[/link]"))
    expected = [
        (0, None, Tag(name="link", parameters="foo")),
        (13, "bar", None),
        (13, None, Tag(name="/link", parameters=None)),
    ]
    assert result == expected


def test_render():
    result = render("[bold]FOO[/bold]")
    assert str(result) == "FOO"
    assert result.spans == [Span(0, 3, "bold")]


def test_render_not_tags():
    result = render('[[1], [1,2,3,4], ["hello"], [None], [False], [True]] []')
    assert str(result) == '[[1], [1,2,3,4], ["hello"], [None], [False], [True]] []'
    assert result.spans == []


def test_render_link():
    result = render("[link=foo]FOO[/link]")
    assert str(result) == "FOO"
    assert result.spans == [Span(0, 3, "link foo")]


def test_render_combine():
    result = render("[green]X[blue]Y[/blue]Z[/green]")
    assert str(result) == "XYZ"
    assert result.spans == [
        Span(0, 3, "green"),
        Span(1, 2, "blue"),
    ]


def test_render_overlap():
    result = render("[green]X[bold]Y[/green]Z[/bold]")
    assert str(result) == "XYZ"
    assert result.spans == [
        Span(0, 2, "green"),
        Span(1, 3, "bold"),
    ]


def test_adjoint():
    result = render("[red][blue]B[/blue]R[/red]")
    print(repr(result))
    assert result.spans == [Span(0, 2, "red"), Span(0, 1, "blue")]


def test_render_close():
    result = render("[bold]X[/]Y")
    assert str(result) == "XY"
    assert result.spans == [Span(0, 1, "bold")]


def test_render_close_ambiguous():
    result = render("[green]X[bold]Y[/]Z[/]")
    assert str(result) == "XYZ"
    assert result.spans == [Span(0, 3, "green"), Span(1, 2, "bold")]


def test_markup_error():
    with pytest.raises(MarkupError):
        assert render("foo[/]")
    with pytest.raises(MarkupError):
        assert render("foo[/bar]")
    with pytest.raises(MarkupError):
        assert render("[foo]hello[/bar]")


def test_markup_escape():
    result = str(render("[dim white][url=[/]"))
    assert result == "[url="


def test_escape_escape():
    # Escaped escapes (i.e. double backslash)should be treated as literal
    result = render(r"\\[bold]FOO")
    assert str(result) == r"\FOO"

    # Single backslash makes the tag literal
    result = render(r"\[bold]FOO")
    assert str(result) == "[bold]FOO"

    # Double backslash produces a backslash
    result = render(r"\\[bold]some text[/]")
    assert str(result) == r"\some text"

    # Triple backslash parsed as literal backslash plus escaped tag
    result = render(r"\\\[bold]some text\[/]")
    assert str(result) == r"\[bold]some text[/]"

    # Backslash escaping only happens when preceding a tag
    result = render(r"\\")
    assert str(result) == r"\\"

    result = render(r"\\\\")
    assert str(result) == r"\\\\"


def test_events():
    result = render("[@click]Hello[/@click] [@click='view.toggle', 'left']World[/]")
    assert str(result) == "Hello World"


def test_events_broken():
    with pytest.raises(MarkupError):
        render("[@click=sdfwer(sfs)]foo[/]")

    with pytest.raises(MarkupError):
        render("[@click='view.toggle]foo[/]")


def test_render_meta():
    console = Console()
    text = render("foo[@click=close]bar[/]baz")
    assert text.get_style_at_offset(console, 3).meta == {"@click": ("close", ())}

    text = render("foo[@click=close()]bar[/]baz")
    assert text.get_style_at_offset(console, 3).meta == {"@click": ("close", ())}

    text = render("foo[@click=close('dialog')]bar[/]baz")
    assert text.get_style_at_offset(console, 3).meta == {
        "@click": ("close", ("dialog",))
    }
    text = render("foo[@click=close('dialog', 3)]bar[/]baz")
    assert text.get_style_at_offset(console, 3).meta == {
        "@click": ("close", ("dialog", 3))
    }

    text = render("foo[@click=(1, 2, 3)]bar[/]baz")
    assert text.get_style_at_offset(console, 3).meta == {"@click": (1, 2, 3)}