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