Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/style.py: 48%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import sys
2from functools import lru_cache
3from operator import attrgetter
4from pickle import dumps, loads
5from random import randint
6from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast
8from . import errors
9from .color import Color, ColorParseError, ColorSystem, blend_rgb
10from .repr import Result, rich_repr
11from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
13_hash_getter = attrgetter(
14 "_color", "_bgcolor", "_attributes", "_set_attributes", "_link", "_meta"
15)
17# Style instances and style definitions are often interchangeable
18StyleType = Union[str, "Style"]
21class _Bit:
22 """A descriptor to get/set a style attribute bit."""
24 __slots__ = ["bit"]
26 def __init__(self, bit_no: int) -> None:
27 self.bit = 1 << bit_no
29 def __get__(self, obj: "Style", objtype: Type["Style"]) -> Optional[bool]:
30 if obj._set_attributes & self.bit:
31 return obj._attributes & self.bit != 0
32 return None
35@rich_repr
36class Style:
37 """A terminal style.
39 A terminal style consists of a color (`color`), a background color (`bgcolor`), and a number of attributes, such
40 as bold, italic etc. The attributes have 3 states: they can either be on
41 (``True``), off (``False``), or not set (``None``).
43 Args:
44 color (Union[Color, str], optional): Color of terminal text. Defaults to None.
45 bgcolor (Union[Color, str], optional): Color of terminal background. Defaults to None.
46 bold (bool, optional): Enable bold text. Defaults to None.
47 dim (bool, optional): Enable dim text. Defaults to None.
48 italic (bool, optional): Enable italic text. Defaults to None.
49 underline (bool, optional): Enable underlined text. Defaults to None.
50 blink (bool, optional): Enabled blinking text. Defaults to None.
51 blink2 (bool, optional): Enable fast blinking text. Defaults to None.
52 reverse (bool, optional): Enabled reverse text. Defaults to None.
53 conceal (bool, optional): Enable concealed text. Defaults to None.
54 strike (bool, optional): Enable strikethrough text. Defaults to None.
55 underline2 (bool, optional): Enable doubly underlined text. Defaults to None.
56 frame (bool, optional): Enable framed text. Defaults to None.
57 encircle (bool, optional): Enable encircled text. Defaults to None.
58 overline (bool, optional): Enable overlined text. Defaults to None.
59 link (str, link): Link URL. Defaults to None.
61 """
63 _color: Optional[Color]
64 _bgcolor: Optional[Color]
65 _attributes: int
66 _set_attributes: int
67 _hash: Optional[int]
68 _null: bool
69 _meta: Optional[bytes]
71 __slots__ = [
72 "_color",
73 "_bgcolor",
74 "_attributes",
75 "_set_attributes",
76 "_link",
77 "_link_id",
78 "_ansi",
79 "_style_definition",
80 "_hash",
81 "_null",
82 "_meta",
83 ]
85 # maps bits on to SGR parameter
86 _style_map = {
87 0: "1",
88 1: "2",
89 2: "3",
90 3: "4",
91 4: "5",
92 5: "6",
93 6: "7",
94 7: "8",
95 8: "9",
96 9: "21",
97 10: "51",
98 11: "52",
99 12: "53",
100 }
102 STYLE_ATTRIBUTES = {
103 "dim": "dim",
104 "d": "dim",
105 "bold": "bold",
106 "b": "bold",
107 "italic": "italic",
108 "i": "italic",
109 "underline": "underline",
110 "u": "underline",
111 "blink": "blink",
112 "blink2": "blink2",
113 "reverse": "reverse",
114 "r": "reverse",
115 "conceal": "conceal",
116 "c": "conceal",
117 "strike": "strike",
118 "s": "strike",
119 "underline2": "underline2",
120 "uu": "underline2",
121 "frame": "frame",
122 "encircle": "encircle",
123 "overline": "overline",
124 "o": "overline",
125 }
127 def __init__(
128 self,
129 *,
130 color: Optional[Union[Color, str]] = None,
131 bgcolor: Optional[Union[Color, str]] = None,
132 bold: Optional[bool] = None,
133 dim: Optional[bool] = None,
134 italic: Optional[bool] = None,
135 underline: Optional[bool] = None,
136 blink: Optional[bool] = None,
137 blink2: Optional[bool] = None,
138 reverse: Optional[bool] = None,
139 conceal: Optional[bool] = None,
140 strike: Optional[bool] = None,
141 underline2: Optional[bool] = None,
142 frame: Optional[bool] = None,
143 encircle: Optional[bool] = None,
144 overline: Optional[bool] = None,
145 link: Optional[str] = None,
146 meta: Optional[Dict[str, Any]] = None,
147 ):
148 self._ansi: Optional[str] = None
149 self._style_definition: Optional[str] = None
151 def _make_color(color: Union[Color, str]) -> Color:
152 return color if isinstance(color, Color) else Color.parse(color)
154 self._color = None if color is None else _make_color(color)
155 self._bgcolor = None if bgcolor is None else _make_color(bgcolor)
156 self._set_attributes = sum(
157 (
158 bold is not None,
159 dim is not None and 2,
160 italic is not None and 4,
161 underline is not None and 8,
162 blink is not None and 16,
163 blink2 is not None and 32,
164 reverse is not None and 64,
165 conceal is not None and 128,
166 strike is not None and 256,
167 underline2 is not None and 512,
168 frame is not None and 1024,
169 encircle is not None and 2048,
170 overline is not None and 4096,
171 )
172 )
173 self._attributes = (
174 sum(
175 (
176 bold and 1 or 0,
177 dim and 2 or 0,
178 italic and 4 or 0,
179 underline and 8 or 0,
180 blink and 16 or 0,
181 blink2 and 32 or 0,
182 reverse and 64 or 0,
183 conceal and 128 or 0,
184 strike and 256 or 0,
185 underline2 and 512 or 0,
186 frame and 1024 or 0,
187 encircle and 2048 or 0,
188 overline and 4096 or 0,
189 )
190 )
191 if self._set_attributes
192 else 0
193 )
195 self._link = link
196 self._meta = None if meta is None else dumps(meta)
197 self._link_id = (
198 f"{randint(0, 999999)}{hash(self._meta)}" if (link or meta) else ""
199 )
200 self._hash: Optional[int] = None
201 self._null = not (self._set_attributes or color or bgcolor or link or meta)
203 @classmethod
204 def null(cls) -> "Style":
205 """Create an 'null' style, equivalent to Style(), but more performant."""
206 return NULL_STYLE
208 @classmethod
209 def from_color(
210 cls, color: Optional[Color] = None, bgcolor: Optional[Color] = None
211 ) -> "Style":
212 """Create a new style with colors and no attributes.
214 Returns:
215 color (Optional[Color]): A (foreground) color, or None for no color. Defaults to None.
216 bgcolor (Optional[Color]): A (background) color, or None for no color. Defaults to None.
217 """
218 style: Style = cls.__new__(Style)
219 style._ansi = None
220 style._style_definition = None
221 style._color = color
222 style._bgcolor = bgcolor
223 style._set_attributes = 0
224 style._attributes = 0
225 style._link = None
226 style._link_id = ""
227 style._meta = None
228 style._null = not (color or bgcolor)
229 style._hash = None
230 return style
232 @classmethod
233 def from_meta(cls, meta: Optional[Dict[str, Any]]) -> "Style":
234 """Create a new style with meta data.
236 Returns:
237 meta (Optional[Dict[str, Any]]): A dictionary of meta data. Defaults to None.
238 """
239 style: Style = cls.__new__(Style)
240 style._ansi = None
241 style._style_definition = None
242 style._color = None
243 style._bgcolor = None
244 style._set_attributes = 0
245 style._attributes = 0
246 style._link = None
247 style._meta = dumps(meta)
248 style._link_id = f"{randint(0, 999999)}{hash(style._meta)}"
249 style._hash = None
250 style._null = not (meta)
251 return style
253 @classmethod
254 def on(cls, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Style":
255 """Create a blank style with meta information.
257 Example:
258 style = Style.on(click=self.on_click)
260 Args:
261 meta (Optional[Dict[str, Any]], optional): An optional dict of meta information.
262 **handlers (Any): Keyword arguments are translated in to handlers.
264 Returns:
265 Style: A Style with meta information attached.
266 """
267 meta = {} if meta is None else meta
268 meta.update({f"@{key}": value for key, value in handlers.items()})
269 return cls.from_meta(meta)
271 bold = _Bit(0)
272 dim = _Bit(1)
273 italic = _Bit(2)
274 underline = _Bit(3)
275 blink = _Bit(4)
276 blink2 = _Bit(5)
277 reverse = _Bit(6)
278 conceal = _Bit(7)
279 strike = _Bit(8)
280 underline2 = _Bit(9)
281 frame = _Bit(10)
282 encircle = _Bit(11)
283 overline = _Bit(12)
285 @property
286 def link_id(self) -> str:
287 """Get a link id, used in ansi code for links."""
288 return self._link_id
290 def __str__(self) -> str:
291 """Re-generate style definition from attributes."""
292 if self._style_definition is None:
293 attributes: List[str] = []
294 append = attributes.append
295 bits = self._set_attributes
296 if bits & 0b0000000001111:
297 if bits & 1:
298 append("bold" if self.bold else "not bold")
299 if bits & (1 << 1):
300 append("dim" if self.dim else "not dim")
301 if bits & (1 << 2):
302 append("italic" if self.italic else "not italic")
303 if bits & (1 << 3):
304 append("underline" if self.underline else "not underline")
305 if bits & 0b0000111110000:
306 if bits & (1 << 4):
307 append("blink" if self.blink else "not blink")
308 if bits & (1 << 5):
309 append("blink2" if self.blink2 else "not blink2")
310 if bits & (1 << 6):
311 append("reverse" if self.reverse else "not reverse")
312 if bits & (1 << 7):
313 append("conceal" if self.conceal else "not conceal")
314 if bits & (1 << 8):
315 append("strike" if self.strike else "not strike")
316 if bits & 0b1111000000000:
317 if bits & (1 << 9):
318 append("underline2" if self.underline2 else "not underline2")
319 if bits & (1 << 10):
320 append("frame" if self.frame else "not frame")
321 if bits & (1 << 11):
322 append("encircle" if self.encircle else "not encircle")
323 if bits & (1 << 12):
324 append("overline" if self.overline else "not overline")
325 if self._color is not None:
326 append(self._color.name)
327 if self._bgcolor is not None:
328 append("on")
329 append(self._bgcolor.name)
330 if self._link:
331 append("link")
332 append(self._link)
333 self._style_definition = " ".join(attributes) or "none"
334 return self._style_definition
336 def __bool__(self) -> bool:
337 """A Style is false if it has no attributes, colors, or links."""
338 return not self._null
340 def _make_ansi_codes(self, color_system: ColorSystem) -> str:
341 """Generate ANSI codes for this style.
343 Args:
344 color_system (ColorSystem): Color system.
346 Returns:
347 str: String containing codes.
348 """
350 if self._ansi is None:
351 sgr: List[str] = []
352 append = sgr.append
353 _style_map = self._style_map
354 attributes = self._attributes & self._set_attributes
355 if attributes:
356 if attributes & 1:
357 append(_style_map[0])
358 if attributes & 2:
359 append(_style_map[1])
360 if attributes & 4:
361 append(_style_map[2])
362 if attributes & 8:
363 append(_style_map[3])
364 if attributes & 0b0000111110000:
365 for bit in range(4, 9):
366 if attributes & (1 << bit):
367 append(_style_map[bit])
368 if attributes & 0b1111000000000:
369 for bit in range(9, 13):
370 if attributes & (1 << bit):
371 append(_style_map[bit])
372 if self._color is not None:
373 sgr.extend(self._color.downgrade(color_system).get_ansi_codes())
374 if self._bgcolor is not None:
375 sgr.extend(
376 self._bgcolor.downgrade(color_system).get_ansi_codes(
377 foreground=False
378 )
379 )
380 self._ansi = ";".join(sgr)
381 return self._ansi
383 @classmethod
384 @lru_cache(maxsize=1024)
385 def normalize(cls, style: str) -> str:
386 """Normalize a style definition so that styles with the same effect have the same string
387 representation.
389 Args:
390 style (str): A style definition.
392 Returns:
393 str: Normal form of style definition.
394 """
395 try:
396 return str(cls.parse(style))
397 except errors.StyleSyntaxError:
398 return style.strip().lower()
400 @classmethod
401 def pick_first(cls, *values: Optional[StyleType]) -> StyleType:
402 """Pick first non-None style."""
403 for value in values:
404 if value is not None:
405 return value
406 raise ValueError("expected at least one non-None style")
408 def __rich_repr__(self) -> Result:
409 yield "color", self.color, None
410 yield "bgcolor", self.bgcolor, None
411 yield "bold", self.bold, None,
412 yield "dim", self.dim, None,
413 yield "italic", self.italic, None
414 yield "underline", self.underline, None,
415 yield "blink", self.blink, None
416 yield "blink2", self.blink2, None
417 yield "reverse", self.reverse, None
418 yield "conceal", self.conceal, None
419 yield "strike", self.strike, None
420 yield "underline2", self.underline2, None
421 yield "frame", self.frame, None
422 yield "encircle", self.encircle, None
423 yield "link", self.link, None
424 if self._meta:
425 yield "meta", self.meta
427 def __eq__(self, other: Any) -> bool:
428 if not isinstance(other, Style):
429 return NotImplemented
430 return self.__hash__() == other.__hash__()
432 def __ne__(self, other: Any) -> bool:
433 if not isinstance(other, Style):
434 return NotImplemented
435 return self.__hash__() != other.__hash__()
437 def __hash__(self) -> int:
438 if self._hash is not None:
439 return self._hash
440 self._hash = hash(_hash_getter(self))
441 return self._hash
443 @property
444 def color(self) -> Optional[Color]:
445 """The foreground color or None if it is not set."""
446 return self._color
448 @property
449 def bgcolor(self) -> Optional[Color]:
450 """The background color or None if it is not set."""
451 return self._bgcolor
453 @property
454 def link(self) -> Optional[str]:
455 """Link text, if set."""
456 return self._link
458 @property
459 def transparent_background(self) -> bool:
460 """Check if the style specified a transparent background."""
461 return self.bgcolor is None or self.bgcolor.is_default
463 @property
464 def background_style(self) -> "Style":
465 """A Style with background only."""
466 return Style(bgcolor=self.bgcolor)
468 @property
469 def meta(self) -> Dict[str, Any]:
470 """Get meta information (can not be changed after construction)."""
471 return {} if self._meta is None else cast(Dict[str, Any], loads(self._meta))
473 @property
474 def without_color(self) -> "Style":
475 """Get a copy of the style with color removed."""
476 if self._null:
477 return NULL_STYLE
478 style: Style = self.__new__(Style)
479 style._ansi = None
480 style._style_definition = None
481 style._color = None
482 style._bgcolor = None
483 style._attributes = self._attributes
484 style._set_attributes = self._set_attributes
485 style._link = self._link
486 style._link_id = f"{randint(0, 999999)}" if self._link else ""
487 style._null = False
488 style._meta = None
489 style._hash = None
490 return style
492 @classmethod
493 @lru_cache(maxsize=4096)
494 def parse(cls, style_definition: str) -> "Style":
495 """Parse a style definition.
497 Args:
498 style_definition (str): A string containing a style.
500 Raises:
501 errors.StyleSyntaxError: If the style definition syntax is invalid.
503 Returns:
504 `Style`: A Style instance.
505 """
506 if style_definition.strip() == "none" or not style_definition:
507 return cls.null()
509 STYLE_ATTRIBUTES = cls.STYLE_ATTRIBUTES
510 color: Optional[str] = None
511 bgcolor: Optional[str] = None
512 attributes: Dict[str, Optional[Any]] = {}
513 link: Optional[str] = None
515 words = iter(style_definition.split())
516 for original_word in words:
517 word = original_word.lower()
518 if word == "on":
519 word = next(words, "")
520 if not word:
521 raise errors.StyleSyntaxError("color expected after 'on'")
522 try:
523 Color.parse(word)
524 except ColorParseError as error:
525 raise errors.StyleSyntaxError(
526 f"unable to parse {word!r} as background color; {error}"
527 ) from None
528 bgcolor = word
530 elif word == "not":
531 word = next(words, "")
532 attribute = STYLE_ATTRIBUTES.get(word)
533 if attribute is None:
534 raise errors.StyleSyntaxError(
535 f"expected style attribute after 'not', found {word!r}"
536 )
537 attributes[attribute] = False
539 elif word == "link":
540 word = next(words, "")
541 if not word:
542 raise errors.StyleSyntaxError("URL expected after 'link'")
543 link = word
545 elif word in STYLE_ATTRIBUTES:
546 attributes[STYLE_ATTRIBUTES[word]] = True
548 else:
549 try:
550 Color.parse(word)
551 except ColorParseError as error:
552 raise errors.StyleSyntaxError(
553 f"unable to parse {word!r} as color; {error}"
554 ) from None
555 color = word
556 style = Style(color=color, bgcolor=bgcolor, link=link, **attributes)
557 return style
559 @lru_cache(maxsize=1024)
560 def get_html_style(self, theme: Optional[TerminalTheme] = None) -> str:
561 """Get a CSS style rule."""
562 theme = theme or DEFAULT_TERMINAL_THEME
563 css: List[str] = []
564 append = css.append
566 color = self.color
567 bgcolor = self.bgcolor
568 if self.reverse:
569 color, bgcolor = bgcolor, color
570 if self.dim:
571 foreground_color = (
572 theme.foreground_color if color is None else color.get_truecolor(theme)
573 )
574 color = Color.from_triplet(
575 blend_rgb(foreground_color, theme.background_color, 0.5)
576 )
577 if color is not None:
578 theme_color = color.get_truecolor(theme)
579 append(f"color: {theme_color.hex}")
580 append(f"text-decoration-color: {theme_color.hex}")
581 if bgcolor is not None:
582 theme_color = bgcolor.get_truecolor(theme, foreground=False)
583 append(f"background-color: {theme_color.hex}")
584 if self.bold:
585 append("font-weight: bold")
586 if self.italic:
587 append("font-style: italic")
588 if self.underline:
589 append("text-decoration: underline")
590 if self.strike:
591 append("text-decoration: line-through")
592 if self.overline:
593 append("text-decoration: overline")
594 return "; ".join(css)
596 @classmethod
597 def combine(cls, styles: Iterable["Style"]) -> "Style":
598 """Combine styles and get result.
600 Args:
601 styles (Iterable[Style]): Styles to combine.
603 Returns:
604 Style: A new style instance.
605 """
606 iter_styles = iter(styles)
607 return sum(iter_styles, next(iter_styles))
609 @classmethod
610 def chain(cls, *styles: "Style") -> "Style":
611 """Combine styles from positional argument in to a single style.
613 Args:
614 *styles (Iterable[Style]): Styles to combine.
616 Returns:
617 Style: A new style instance.
618 """
619 iter_styles = iter(styles)
620 return sum(iter_styles, next(iter_styles))
622 def copy(self) -> "Style":
623 """Get a copy of this style.
625 Returns:
626 Style: A new Style instance with identical attributes.
627 """
628 if self._null:
629 return NULL_STYLE
630 style: Style = self.__new__(Style)
631 style._ansi = self._ansi
632 style._style_definition = self._style_definition
633 style._color = self._color
634 style._bgcolor = self._bgcolor
635 style._attributes = self._attributes
636 style._set_attributes = self._set_attributes
637 style._link = self._link
638 style._link_id = f"{randint(0, 999999)}" if self._link else ""
639 style._hash = self._hash
640 style._null = False
641 style._meta = self._meta
642 return style
644 @lru_cache(maxsize=128)
645 def clear_meta_and_links(self) -> "Style":
646 """Get a copy of this style with link and meta information removed.
648 Returns:
649 Style: New style object.
650 """
651 if self._null:
652 return NULL_STYLE
653 style: Style = self.__new__(Style)
654 style._ansi = self._ansi
655 style._style_definition = self._style_definition
656 style._color = self._color
657 style._bgcolor = self._bgcolor
658 style._attributes = self._attributes
659 style._set_attributes = self._set_attributes
660 style._link = None
661 style._link_id = ""
662 style._hash = None
663 style._null = False
664 style._meta = None
665 return style
667 def update_link(self, link: Optional[str] = None) -> "Style":
668 """Get a copy with a different value for link.
670 Args:
671 link (str, optional): New value for link. Defaults to None.
673 Returns:
674 Style: A new Style instance.
675 """
676 style: Style = self.__new__(Style)
677 style._ansi = self._ansi
678 style._style_definition = self._style_definition
679 style._color = self._color
680 style._bgcolor = self._bgcolor
681 style._attributes = self._attributes
682 style._set_attributes = self._set_attributes
683 style._link = link
684 style._link_id = f"{randint(0, 999999)}" if link else ""
685 style._hash = None
686 style._null = False
687 style._meta = self._meta
688 return style
690 def render(
691 self,
692 text: str = "",
693 *,
694 color_system: Optional[ColorSystem] = ColorSystem.TRUECOLOR,
695 legacy_windows: bool = False,
696 ) -> str:
697 """Render the ANSI codes for the style.
699 Args:
700 text (str, optional): A string to style. Defaults to "".
701 color_system (Optional[ColorSystem], optional): Color system to render to. Defaults to ColorSystem.TRUECOLOR.
703 Returns:
704 str: A string containing ANSI style codes.
705 """
706 if not text or color_system is None:
707 return text
708 attrs = self._ansi or self._make_ansi_codes(color_system)
709 rendered = f"\x1b[{attrs}m{text}\x1b[0m" if attrs else text
710 if self._link and not legacy_windows:
711 rendered = (
712 f"\x1b]8;id={self._link_id};{self._link}\x1b\\{rendered}\x1b]8;;\x1b\\"
713 )
714 return rendered
716 def test(self, text: Optional[str] = None) -> None:
717 """Write text with style directly to terminal.
719 This method is for testing purposes only.
721 Args:
722 text (Optional[str], optional): Text to style or None for style name.
724 """
725 text = text or str(self)
726 sys.stdout.write(f"{self.render(text)}\n")
728 @lru_cache(maxsize=1024)
729 def _add(self, style: Optional["Style"]) -> "Style":
730 if style is None or style._null:
731 return self
732 if self._null:
733 return style
734 new_style: Style = self.__new__(Style)
735 new_style._ansi = None
736 new_style._style_definition = None
737 new_style._color = style._color or self._color
738 new_style._bgcolor = style._bgcolor or self._bgcolor
739 new_style._attributes = (self._attributes & ~style._set_attributes) | (
740 style._attributes & style._set_attributes
741 )
742 new_style._set_attributes = self._set_attributes | style._set_attributes
743 new_style._link = style._link or self._link
744 new_style._link_id = style._link_id or self._link_id
745 new_style._null = style._null
746 if self._meta and style._meta:
747 new_style._meta = dumps({**self.meta, **style.meta})
748 else:
749 new_style._meta = self._meta or style._meta
750 new_style._hash = None
751 return new_style
753 def __add__(self, style: Optional["Style"]) -> "Style":
754 combined_style = self._add(style)
755 return combined_style.copy() if combined_style.link else combined_style
758NULL_STYLE = Style()
761class StyleStack:
762 """A stack of styles."""
764 __slots__ = ["_stack"]
766 def __init__(self, default_style: "Style") -> None:
767 self._stack: List[Style] = [default_style]
769 def __repr__(self) -> str:
770 return f"<stylestack {self._stack!r}>"
772 @property
773 def current(self) -> Style:
774 """Get the Style at the top of the stack."""
775 return self._stack[-1]
777 def push(self, style: Style) -> None:
778 """Push a new style on to the stack.
780 Args:
781 style (Style): New style to combine with current style.
782 """
783 self._stack.append(self._stack[-1] + style)
785 def pop(self) -> Style:
786 """Pop last style and discard.
788 Returns:
789 Style: New current style (also available as stack.current)
790 """
791 self._stack.pop()
792 return self._stack[-1]