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

431 statements  

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 

7 

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 

12 

13_hash_getter = attrgetter( 

14 "_color", "_bgcolor", "_attributes", "_set_attributes", "_link", "_meta" 

15) 

16 

17# Style instances and style definitions are often interchangeable 

18StyleType = Union[str, "Style"] 

19 

20 

21class _Bit: 

22 """A descriptor to get/set a style attribute bit.""" 

23 

24 __slots__ = ["bit"] 

25 

26 def __init__(self, bit_no: int) -> None: 

27 self.bit = 1 << bit_no 

28 

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 

33 

34 

35@rich_repr 

36class Style: 

37 """A terminal style. 

38 

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``). 

42 

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. 

60 

61 """ 

62 

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] 

70 

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 ] 

84 

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 } 

101 

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 } 

126 

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 

150 

151 def _make_color(color: Union[Color, str]) -> Color: 

152 return color if isinstance(color, Color) else Color.parse(color) 

153 

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 ) 

194 

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) 

202 

203 @classmethod 

204 def null(cls) -> "Style": 

205 """Create an 'null' style, equivalent to Style(), but more performant.""" 

206 return NULL_STYLE 

207 

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. 

213 

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 

231 

232 @classmethod 

233 def from_meta(cls, meta: Optional[Dict[str, Any]]) -> "Style": 

234 """Create a new style with meta data. 

235 

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 

252 

253 @classmethod 

254 def on(cls, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Style": 

255 """Create a blank style with meta information. 

256 

257 Example: 

258 style = Style.on(click=self.on_click) 

259 

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. 

263 

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) 

270 

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) 

284 

285 @property 

286 def link_id(self) -> str: 

287 """Get a link id, used in ansi code for links.""" 

288 return self._link_id 

289 

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 

335 

336 def __bool__(self) -> bool: 

337 """A Style is false if it has no attributes, colors, or links.""" 

338 return not self._null 

339 

340 def _make_ansi_codes(self, color_system: ColorSystem) -> str: 

341 """Generate ANSI codes for this style. 

342 

343 Args: 

344 color_system (ColorSystem): Color system. 

345 

346 Returns: 

347 str: String containing codes. 

348 """ 

349 

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 

382 

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. 

388 

389 Args: 

390 style (str): A style definition. 

391 

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() 

399 

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") 

407 

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 

426 

427 def __eq__(self, other: Any) -> bool: 

428 if not isinstance(other, Style): 

429 return NotImplemented 

430 return self.__hash__() == other.__hash__() 

431 

432 def __ne__(self, other: Any) -> bool: 

433 if not isinstance(other, Style): 

434 return NotImplemented 

435 return self.__hash__() != other.__hash__() 

436 

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 

442 

443 @property 

444 def color(self) -> Optional[Color]: 

445 """The foreground color or None if it is not set.""" 

446 return self._color 

447 

448 @property 

449 def bgcolor(self) -> Optional[Color]: 

450 """The background color or None if it is not set.""" 

451 return self._bgcolor 

452 

453 @property 

454 def link(self) -> Optional[str]: 

455 """Link text, if set.""" 

456 return self._link 

457 

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 

462 

463 @property 

464 def background_style(self) -> "Style": 

465 """A Style with background only.""" 

466 return Style(bgcolor=self.bgcolor) 

467 

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)) 

472 

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 

491 

492 @classmethod 

493 @lru_cache(maxsize=4096) 

494 def parse(cls, style_definition: str) -> "Style": 

495 """Parse a style definition. 

496 

497 Args: 

498 style_definition (str): A string containing a style. 

499 

500 Raises: 

501 errors.StyleSyntaxError: If the style definition syntax is invalid. 

502 

503 Returns: 

504 `Style`: A Style instance. 

505 """ 

506 if style_definition.strip() == "none" or not style_definition: 

507 return cls.null() 

508 

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 

514 

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 

529 

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 

538 

539 elif word == "link": 

540 word = next(words, "") 

541 if not word: 

542 raise errors.StyleSyntaxError("URL expected after 'link'") 

543 link = word 

544 

545 elif word in STYLE_ATTRIBUTES: 

546 attributes[STYLE_ATTRIBUTES[word]] = True 

547 

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 

558 

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 

565 

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) 

595 

596 @classmethod 

597 def combine(cls, styles: Iterable["Style"]) -> "Style": 

598 """Combine styles and get result. 

599 

600 Args: 

601 styles (Iterable[Style]): Styles to combine. 

602 

603 Returns: 

604 Style: A new style instance. 

605 """ 

606 iter_styles = iter(styles) 

607 return sum(iter_styles, next(iter_styles)) 

608 

609 @classmethod 

610 def chain(cls, *styles: "Style") -> "Style": 

611 """Combine styles from positional argument in to a single style. 

612 

613 Args: 

614 *styles (Iterable[Style]): Styles to combine. 

615 

616 Returns: 

617 Style: A new style instance. 

618 """ 

619 iter_styles = iter(styles) 

620 return sum(iter_styles, next(iter_styles)) 

621 

622 def copy(self) -> "Style": 

623 """Get a copy of this style. 

624 

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 

643 

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. 

647 

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 

666 

667 def update_link(self, link: Optional[str] = None) -> "Style": 

668 """Get a copy with a different value for link. 

669 

670 Args: 

671 link (str, optional): New value for link. Defaults to None. 

672 

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 

689 

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. 

698 

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. 

702 

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 

715 

716 def test(self, text: Optional[str] = None) -> None: 

717 """Write text with style directly to terminal. 

718 

719 This method is for testing purposes only. 

720 

721 Args: 

722 text (Optional[str], optional): Text to style or None for style name. 

723 

724 """ 

725 text = text or str(self) 

726 sys.stdout.write(f"{self.render(text)}\n") 

727 

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 

752 

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 

756 

757 

758NULL_STYLE = Style() 

759 

760 

761class StyleStack: 

762 """A stack of styles.""" 

763 

764 __slots__ = ["_stack"] 

765 

766 def __init__(self, default_style: "Style") -> None: 

767 self._stack: List[Style] = [default_style] 

768 

769 def __repr__(self) -> str: 

770 return f"<stylestack {self._stack!r}>" 

771 

772 @property 

773 def current(self) -> Style: 

774 """Get the Style at the top of the stack.""" 

775 return self._stack[-1] 

776 

777 def push(self, style: Style) -> None: 

778 """Push a new style on to the stack. 

779 

780 Args: 

781 style (Style): New style to combine with current style. 

782 """ 

783 self._stack.append(self._stack[-1] + style) 

784 

785 def pop(self) -> Style: 

786 """Pop last style and discard. 

787 

788 Returns: 

789 Style: New current style (also available as stack.current) 

790 """ 

791 self._stack.pop() 

792 return self._stack[-1]