diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e43293b8..938d4628b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,11 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [10.3.1] - Unreleased
+## [10.4.0] - 2021-06-18
### Added
- Added Style.meta
+- Added rich.repr.auto decorator
### Fixed
diff --git a/docs/source/pretty.rst b/docs/source/pretty.rst
index 01fdcd613..7929150a0 100644
--- a/docs/source/pretty.rst
+++ b/docs/source/pretty.rst
@@ -67,24 +67,28 @@ Rich Repr Protocol
Rich is able to syntax highlight any output, but the formatting is restricted to builtin containers, dataclasses, and other objects Rich knows about, such as objects generated by the `attrs `_ library. To add Rich formatting capabilities to custom objects, you can implement the *rich repr protocol*.
+Run the following command to see an example of what the Rich repr protocol can generate::
+
+ python -m rich.repr
+
First, let's look at a class that might benefit from a Rich repr::
- class Bird:
- def __init__(self, name, eats=None, fly=True, extinct=False):
- self.name = name
- self.eats = list(eats) if eats else []
- self.fly = fly
- self.extinct = extinct
+class Bird:
+ def __init__(self, name, eats=None, fly=True, extinct=False):
+ self.name = name
+ self.eats = list(eats) if eats else []
+ self.fly = fly
+ self.extinct = extinct
- def __repr__(self):
- return f"Bird({self.name!r}, eats={self.eats!r}, fly={self.fly!r}, extinct={self.extinct!r})"
+ def __repr__(self):
+ return f"Bird({self.name!r}, eats={self.eats!r}, fly={self.fly!r}, extinct={self.extinct!r})"
- BIRDS = {
- "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]),
- "penguin": Bird("penguin", eats=["fish"], fly=False),
- "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True)
- }
- print(BIRDS)
+BIRDS = {
+ "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]),
+ "penguin": Bird("penguin", eats=["fish"], fly=False),
+ "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True)
+}
+print(BIRDS)
The result of this script would be::
@@ -168,6 +172,45 @@ This will change the output of the Rich repr example to the following::
Note that you can add ``__rich_repr__`` methods to third-party libraries *without* including Rich as a dependency. If Rich is not installed, then nothing will break. Hopefully more third-party libraries will adopt Rich repr methods in the future.
+Automatic Rich Repr
+~~~~~~~~~~~~~~~~~~~
+
+Rich can generate a rich repr automatically if the parameters are named the same as your attributes.
+
+To automatically build a rich repr, use the :meth:`~rich.repr.auto` class decorator. The Bird example above follows the above rule, so we wouldn't even need to implement our own `__rich_repr__`::
+
+ import rich.repr
+
+ @rich.repr.auto
+ class Bird:
+ def __init__(self, name, eats=None, fly=True, extinct=False):
+ self.name = name
+ self.eats = list(eats) if eats else []
+ self.fly = fly
+ self.extinct = extinct
+
+
+ BIRDS = {
+ "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]),
+ "penguin": Bird("penguin", eats=["fish"], fly=False),
+ "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True)
+ }
+ from rich import print
+ print(BIRDS)
+
+Note that the decorator will also create a `__repr__`, so you you will get an auto-generated repr even if you don't print with Rich.
+
+If you want to auto-generate the angular type of repr, then set ``angular=True`` on the decorator::
+
+ @rich.repr.auto(angular=True)
+ class Bird:
+ def __init__(self, name, eats=None, fly=True, extinct=False):
+ self.name = name
+ self.eats = list(eats) if eats else []
+ self.fly = fly
+ self.extinct = extinct
+
+
Example
-------
diff --git a/examples/repr.py b/examples/repr.py
index 7af5bdcd2..01226fc5f 100644
--- a/examples/repr.py
+++ b/examples/repr.py
@@ -1,7 +1,7 @@
-from rich.repr import rich_repr
+import rich.repr
-@rich_repr
+@rich.repr.auto
class Bird:
def __init__(self, name, eats=None, fly=True, extinct=False):
self.name = name
@@ -9,14 +9,9 @@ def __init__(self, name, eats=None, fly=True, extinct=False):
self.fly = fly
self.extinct = extinct
- def __rich_repr__(self):
- yield self.name
- yield "eats", self.eats
- yield "fly", self.fly, True
- yield "extinct", self.extinct, False
-
- # __rich_repr__.angular = True
+# Note that the repr is still generate without Rich
+# Try commenting out the following lin
from rich import print
diff --git a/pyproject.toml b/pyproject.toml
index 3ce74bee5..ad4cbb688 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
-version = "10.3.0"
+version = "10.4.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan "]
license = "MIT"
diff --git a/rich/color.py b/rich/color.py
index a8032ee0d..95cad68c4 100644
--- a/rich/color.py
+++ b/rich/color.py
@@ -7,6 +7,7 @@
from ._palettes import EIGHT_BIT_PALETTE, STANDARD_PALETTE, WINDOWS_PALETTE
from .color_triplet import ColorTriplet
+from .repr import rich_repr, RichReprResult
from .terminal_theme import DEFAULT_TERMINAL_THEME
if TYPE_CHECKING: # pragma: no cover
@@ -25,6 +26,9 @@ class ColorSystem(IntEnum):
TRUECOLOR = 3
WINDOWS = 4
+ def __repr__(self) -> str:
+ return f"ColorSystem.{self.name}"
+
class ColorType(IntEnum):
"""Type of color stored in Color class."""
@@ -35,6 +39,9 @@ class ColorType(IntEnum):
TRUECOLOR = 3
WINDOWS = 4
+ def __repr__(self) -> str:
+ return f"ColorType.{self.name}"
+
ANSI_COLOR_NAMES = {
"black": 0,
@@ -257,6 +264,7 @@ class ColorParseError(Exception):
)
+@rich_repr
class Color(NamedTuple):
"""Terminal color definition."""
@@ -269,13 +277,6 @@ class Color(NamedTuple):
triplet: Optional[ColorTriplet] = None
"""A triplet of color components, if an RGB color."""
- def __repr__(self) -> str:
- return (
- f""
- if self.number is None
- else f""
- )
-
def __rich__(self) -> "Text":
"""Dispays the actual color if Rich printed."""
from .text import Text
@@ -287,6 +288,12 @@ def __rich__(self) -> "Text":
" >",
)
+ def __rich_repr__(self) -> RichReprResult:
+ yield self.name
+ yield self.type
+ yield "number", self.number, None
+ yield "triplet", self.triplet, None
+
@property
def system(self) -> ColorSystem:
"""Get the native color system for this color."""
diff --git a/rich/markup.py b/rich/markup.py
index 179cabc57..159b104cb 100644
--- a/rich/markup.py
+++ b/rich/markup.py
@@ -1,6 +1,10 @@
+from ast import literal_eval
+from operator import attrgetter
import re
from typing import Callable, Iterable, List, Match, NamedTuple, Optional, Tuple, Union
+from black import E
+
from .errors import MarkupError
from .style import Style
from .text import Span, Text
@@ -8,7 +12,7 @@
RE_TAGS = re.compile(
- r"""((\\*)\[([a-z#\/].*?)\])""",
+ r"""((\\*)\[([a-z#\/@].*?)\])""",
re.VERBOSE,
)
@@ -137,6 +141,7 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
elif tag is not None:
if tag.name.startswith("/"): # Closing tag
style_name = tag.name[1:].strip()
+
if style_name: # explicit close
style_name = normalize(style_name)
try:
@@ -153,7 +158,30 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
f"closing tag '[/]' at position {position} has nothing to close"
) from None
- append_span(_Span(start, len(text), str(open_tag)))
+ if open_tag.name.startswith("@"):
+ if open_tag.parameters:
+ try:
+ meta_params = literal_eval(open_tag.parameters)
+ except SyntaxError as error:
+ raise MarkupError(
+ f"error parsing {open_tag.parameters!r}; {error.msg}"
+ )
+ except Exception as error:
+ raise MarkupError(
+ f"error parsing {open_tag.parameters!r}; {error}"
+ ) from None
+
+ else:
+ meta_params = ()
+
+ append_span(
+ _Span(
+ start, len(text), Style(meta={open_tag.name: meta_params})
+ )
+ )
+ else:
+ append_span(_Span(start, len(text), str(open_tag)))
+
else: # Opening tag
normalized_tag = _Tag(normalize(tag.name), tag.parameters)
style_stack.append((len(text), normalized_tag))
@@ -165,7 +193,7 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
if style:
append_span(_Span(start, text_length, style))
- text.spans = sorted(spans)
+ text.spans = sorted(spans, key=attrgetter("start", "end"))
return text
@@ -174,7 +202,11 @@ def pop_style(style_name: str) -> Tuple[int, Tag]:
from rich.console import Console
from rich.text import Text
- console = Console(highlight=False)
+ console = Console(highlight=True)
+
+ t = render("[b]Hello[/b] [@click='view.toggle', 'left']World[/]")
+ console.print(t)
+ console.print(t._spans)
console.print("Hello [1], [1,2,3] ['hello']")
console.print("foo")
diff --git a/rich/repr.py b/rich/repr.py
index a6d164c97..2236412c8 100644
--- a/rich/repr.py
+++ b/rich/repr.py
@@ -1,4 +1,17 @@
-from typing import Any, Iterable, List, Union, Tuple, Type, TypeVar
+import inspect
+
+from typing import (
+ Any,
+ Callable,
+ Iterable,
+ List,
+ Optional,
+ overload,
+ Union,
+ Tuple,
+ Type,
+ TypeVar,
+)
T = TypeVar("T")
@@ -7,53 +20,101 @@
RichReprResult = Iterable[Union[Any, Tuple[Any], Tuple[str, Any], Tuple[str, Any, Any]]]
-def rich_repr(cls: Type[T]) -> Type[T]:
+class ReprError(Exception):
+ """An error occurred when attempting to build a repr."""
+
+
+@overload
+def auto(cls: Type[T]) -> Type[T]:
+ ...
+
+
+@overload
+def auto(*, angular: bool = False) -> Callable[[Type[T]], Type[T]]:
+ ...
+
+
+def auto(
+ cls: Optional[Type[T]] = None, *, angular: bool = False
+) -> Union[Type[T], Callable[[Type[T]], Type[T]]]:
"""Class decorator to create __repr__ from __rich_repr__"""
- def auto_repr(self: Any) -> str:
- repr_str: List[str] = []
- append = repr_str.append
- angular = getattr(self.__rich_repr__, "angular", False)
- for arg in self.__rich_repr__():
- if isinstance(arg, tuple):
- if len(arg) == 1:
- append(repr(arg[0]))
- else:
- key, value, *default = arg
- if key is None:
- append(repr(value))
+ def do_replace(cls: Type[T]) -> Type[T]:
+ def auto_repr(self: T) -> str:
+ """Create repr string from __rich_repr__"""
+ repr_str: List[str] = []
+ append = repr_str.append
+
+ angular = getattr(self.__rich_repr__, "angular", False) # type: ignore
+ for arg in self.__rich_repr__(): # type: ignore
+ if isinstance(arg, tuple):
+ if len(arg) == 1:
+ append(repr(arg[0]))
else:
- if len(default) and default[0] == value:
- continue
- append(f"{key}={value!r}")
+ key, value, *default = arg
+ if key is None:
+ append(repr(value))
+ else:
+ if len(default) and default[0] == value:
+ continue
+ append(f"{key}={value!r}")
+ else:
+ append(repr(arg))
+ if angular:
+ return f"<{self.__class__.__name__} {' '.join(repr_str)}>"
else:
- append(repr(arg))
- if angular:
- return f"<{self.__class__.__name__} {' '.join(repr_str)}>"
- else:
- return f"{self.__class__.__name__}({', '.join(repr_str)})"
-
- auto_repr.__doc__ = "Return repr(self)"
- cls.__repr__ = auto_repr # type: ignore
-
- return cls
+ return f"{self.__class__.__name__}({', '.join(repr_str)})"
+
+ def auto_rich_repr(self: T) -> RichReprResult:
+ """Auto generate __rich_rep__ from signature of __init__"""
+ try:
+ signature = inspect.signature(self.__init__) # type: ignore
+ for name, param in signature.parameters.items():
+ if param.kind == param.POSITIONAL_ONLY:
+ yield getattr(self, name)
+ elif param.kind in (
+ param.POSITIONAL_OR_KEYWORD,
+ param.KEYWORD_ONLY,
+ ):
+ if param.default == param.empty:
+ yield getattr(self, param.name)
+ else:
+ yield param.name, getattr(self, param.name), param.default
+ except Exception as error:
+ raise ReprError(
+ f"Failed to auto generate __rich_repr__; {error}"
+ ) from None
+
+ if not hasattr(cls, "__rich_repr__"):
+ auto_rich_repr.__doc__ = "Build a rich repr"
+ cls.__rich_repr__ = auto_rich_repr # type: ignore
+ cls.__rich_repr__.angular = angular # type: ignore
+
+ auto_repr.__doc__ = "Return repr(self)"
+ cls.__repr__ = auto_repr # type: ignore
+ return cls
+
+ if cls is None:
+ angular = angular
+ return do_replace
+ else:
+ return do_replace(cls)
+
+
+rich_repr: Callable[[Type[T]], Type[T]] = auto
if __name__ == "__main__":
- @rich_repr
+ @auto
class Foo:
def __rich_repr__(self) -> RichReprResult:
-
yield "foo"
yield "bar", {"shopping": ["eggs", "ham", "pineapple"]}
yield "buy", "hand sanitizer"
- __rich_repr__.angular = False # type: ignore
-
foo = Foo()
from rich.console import Console
- from rich import print
console = Console()
diff --git a/rich/segment.py b/rich/segment.py
index 741d119ee..58a817599 100644
--- a/rich/segment.py
+++ b/rich/segment.py
@@ -425,16 +425,10 @@ def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
if self.new_lines:
- segments = self.segments
- splice_segments: list["Segment | None"] = [None] * (len(segments) * 2)
- splice_segments[::2] = segments
- splice_segments[1::2] = [Segment.line()] * len(segments)
-
- yield from cast("list[Segment]", splice_segments)
-
- # for segment in self.segments:
- # yield segment
- # yield line
+ line = Segment.line()
+ for segment in self.segments:
+ yield segment
+ yield line
else:
yield from self.segments
diff --git a/rich/style.py b/rich/style.py
index 0db055689..d92df0f68 100644
--- a/rich/style.py
+++ b/rich/style.py
@@ -1,14 +1,16 @@
import sys
from functools import lru_cache
-import marshal
+from marshal import loads as marshal_loads, dumps as marshal_dumps
from random import randint
from time import time
-from typing import Any, Dict, Iterable, List, Optional, Type, Union
+from typing import Any, cast, Dict, Iterable, List, Optional, Type, Union
from . import errors
from .color import Color, ColorParseError, ColorSystem, blend_rgb
+from .repr import rich_repr, RichReprResult
from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
+
# Style instances and style definitions are often interchangeable
StyleType = Union[str, "Style"]
@@ -27,6 +29,7 @@ def __get__(self, obj: "Style", objtype: Type["Style"]) -> Optional[bool]:
return None
+@rich_repr
class Style:
"""A terminal style.
@@ -112,7 +115,7 @@ def __init__(
encircle: Optional[bool] = None,
overline: Optional[bool] = None,
link: Optional[str] = None,
- meta: Optional[Dict[str, str]] = None,
+ meta: Optional[Dict[str, Any]] = None,
):
self._ansi: Optional[str] = None
self._style_definition: Optional[str] = None
@@ -163,7 +166,7 @@ def _make_color(color: Union[Color, str]) -> Color:
self._link = link
self._link_id = f"{time()}-{randint(0, 999999)}" if link else ""
- self._meta = None if meta is None else marshal.dumps(meta)
+ self._meta = None if meta is None else marshal_dumps(meta)
self._hash = hash(
(
self._color,
@@ -349,9 +352,24 @@ def pick_first(cls, *values: Optional[StyleType]) -> StyleType:
return value
raise ValueError("expected at least one non-None style")
- def __repr__(self) -> str:
- """Render a named style differently from an anonymous style."""
- return f'Style.parse("{self}")'
+ def __rich_repr__(self) -> RichReprResult:
+ yield "color", self.color, None
+ yield "bgcolor", self.bgcolor, None
+ yield "bold", self.bold, None,
+ yield "dim", self.dim, None,
+ yield "italic", self.italic, None
+ yield "underline", self.underline, None,
+ yield "blink", self.blink, None
+ yield "blink2", self.blink2, None
+ yield "reverse", self.reverse, None
+ yield "conceal", self.conceal, None
+ yield "strike", self.strike, None
+ yield "underline2", self.underline2, None
+ yield "frame", self.frame, None
+ yield "encircle", self.encircle, None
+ yield "link", self.link, None
+ if self._meta:
+ yield "meta", self.meta
def __eq__(self, other: Any) -> bool:
if not isinstance(other, Style):
@@ -394,9 +412,13 @@ def background_style(self) -> "Style":
return Style(bgcolor=self.bgcolor)
@property
- def meta(self) -> Dict[str, str]:
+ def meta(self) -> Dict[str, Any]:
"""Get meta information (can not be changed after construction)."""
- return {} if self._meta is None else marshal.loads(self._meta)
+ return (
+ {}
+ if self._meta is None
+ else cast(Dict[str, Any], marshal_loads(self._meta))
+ )
@property
def without_color(self) -> "Style":
@@ -460,7 +482,7 @@ def parse(cls, style_definition: str) -> "Style":
}
color: Optional[str] = None
bgcolor: Optional[str] = None
- attributes: Dict[str, Optional[bool]] = {}
+ attributes: Dict[str, Optional[Any]] = {}
link: Optional[str] = None
words = iter(style_definition.split())
@@ -589,7 +611,7 @@ def copy(self) -> "Style":
style._link_id = f"{time()}-{randint(0, 999999)}" if self._link else ""
style._hash = self._hash
style._null = False
- self._meta = self._meta
+ style._meta = self._meta
return style
def update_link(self, link: Optional[str] = None) -> "Style":
@@ -674,11 +696,9 @@ def __add__(self, style: Optional["Style"]) -> "Style":
new_style._hash = style._hash
new_style._null = self._null or style._null
if self._meta and style._meta:
- new_style._meta = marshal.dumps({**self.meta, **style.meta})
- elif self._meta or style._meta:
- new_style._meta = self._meta or style._meta
+ new_style._meta = marshal_dumps({**self.meta, **style.meta})
else:
- new_style._meta = None
+ new_style._meta = self._meta or style._meta
return new_style
diff --git a/rich/text.py b/rich/text.py
index 405e3cea9..d86af945b 100644
--- a/rich/text.py
+++ b/rich/text.py
@@ -53,7 +53,10 @@ class Span(NamedTuple):
"""Style associated with the span."""
def __repr__(self) -> str:
- return f"Span({self.start}, {self.end}, {str(self.style)!r})"
+ if isinstance(self.style, Style) and self.style._meta:
+ return f"Span({self.start}, {self.end}, {self.style!r})"
+ else:
+ return f"Span({self.start}, {self.end}, {str(self.style)!r})"
def __bool__(self) -> bool:
return self.end > self.start
diff --git a/tests/test_color.py b/tests/test_color.py
index 49f344e14..389c00ed3 100644
--- a/tests/test_color.py
+++ b/tests/test_color.py
@@ -14,11 +14,11 @@
def test_str() -> None:
- assert str(Color.parse("red")) == ""
+ assert str(Color.parse("red")) == "Color('red', ColorType.STANDARD, number=1)"
def test_repr() -> None:
- assert repr(Color.parse("red")) == ""
+ assert repr(Color.parse("red")) == "Color('red', ColorType.STANDARD, number=1)"
def test_rich() -> None:
diff --git a/tests/test_console.py b/tests/test_console.py
index 0dd992f28..80d21dca4 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -663,4 +663,4 @@ def test_size_properties():
console.width = 5
assert console.size == ConsoleDimensions(5, 20)
console.height = 10
- assert console.size == ConsoleDimensions(5, 10)
\ No newline at end of file
+ assert console.size == ConsoleDimensions(5, 10)
diff --git a/tests/test_control.py b/tests/test_control.py
index 1273732e1..dbd21fe11 100644
--- a/tests/test_control.py
+++ b/tests/test_control.py
@@ -44,4 +44,4 @@ def test_move_to_column():
"\x1b[11G\x1b[20A",
None,
[(ControlType.CURSOR_MOVE_TO_COLUMN, 10), (ControlType.CURSOR_UP, 20)],
- )
\ No newline at end of file
+ )
diff --git a/tests/test_repr.py b/tests/test_repr.py
index 704147f51..c41de9926 100644
--- a/tests/test_repr.py
+++ b/tests/test_repr.py
@@ -1,10 +1,11 @@
+import pytest
from typing import Optional
from rich.console import Console
-from rich.repr import rich_repr
+import rich.repr
-@rich_repr
+@rich.repr.auto
class Foo:
def __init__(self, foo: str, bar: Optional[int] = None, egg: int = 1):
self.foo = foo
@@ -18,7 +19,31 @@ def __rich_repr__(self):
yield "egg", self.egg
-@rich_repr
+@rich.repr.auto
+class Egg:
+ def __init__(self, foo: str, /, bar: Optional[int] = None, egg: int = 1):
+ self.foo = foo
+ self.bar = bar
+ self.egg = egg
+
+
+@rich.repr.auto
+class BrokenEgg:
+ def __init__(self, foo: str, *, bar: Optional[int] = None, egg: int = 1):
+ self.foo = foo
+ self.fubar = bar
+ self.egg = egg
+
+
+@rich.repr.auto(angular=True)
+class AngularEgg:
+ def __init__(self, foo: str, *, bar: Optional[int] = None, egg: int = 1):
+ self.foo = foo
+ self.bar = bar
+ self.egg = egg
+
+
+@rich.repr.auto
class Bar(Foo):
def __rich_repr__(self):
yield (self.foo,)
@@ -39,6 +64,19 @@ def test_rich_angular() -> None:
assert (repr(Bar("hello", bar=3))) == ""
+def test_rich_repr_auto() -> None:
+ assert repr(Egg("hello", egg=2)) == "Egg('hello', egg=2)"
+
+
+def test_rich_repr_auto_angular() -> None:
+ assert repr(AngularEgg("hello", egg=2)) == ""
+
+
+def test_broken_egg() -> None:
+ with pytest.raises(rich.repr.ReprError):
+ repr(BrokenEgg("foo"))
+
+
def test_rich_pretty() -> None:
console = Console()
with console.capture() as capture:
diff --git a/tests/test_style.py b/tests/test_style.py
index 676caf961..d64d3783b 100644
--- a/tests/test_style.py
+++ b/tests/test_style.py
@@ -58,7 +58,10 @@ def test_ansi_codes():
def test_repr():
- assert repr(Style(bold=True, color="red")) == 'Style.parse("bold red")'
+ assert (
+ repr(Style(bold=True, color="red"))
+ == "Style(color=Color('red', ColorType.STANDARD, number=1), bold=True)"
+ )
def test_eq():
@@ -205,3 +208,14 @@ def test_without_color():
assert colorless_style.bold == True
null_style = Style.null()
assert null_style.without_color == null_style
+
+
+def test_meta():
+ style = Style(bold=True, meta={"foo": "bar"})
+ assert style.meta["foo"] == "bar"
+
+ style += Style(meta={"egg": "baz"})
+
+ assert style.meta == {"foo": "bar", "egg": "baz"}
+
+ assert repr(style) == "Style(bold=True, meta={'foo': 'bar', 'egg': 'baz'})"