Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/rich/syntax.py: 57%

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

344 statements  

1from __future__ import annotations 

2 

3import os.path 

4import re 

5import sys 

6import textwrap 

7from abc import ABC, abstractmethod 

8from pathlib import Path 

9from typing import ( 

10 Any, 

11 Dict, 

12 Iterable, 

13 List, 

14 NamedTuple, 

15 Optional, 

16 Sequence, 

17 Set, 

18 Tuple, 

19 Type, 

20 Union, 

21) 

22 

23from pygments.lexer import Lexer 

24from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename 

25from pygments.style import Style as PygmentsStyle 

26from pygments.styles import get_style_by_name 

27from pygments.token import ( 

28 Comment, 

29 Error, 

30 Generic, 

31 Keyword, 

32 Name, 

33 Number, 

34 Operator, 

35 String, 

36 Token, 

37 Whitespace, 

38) 

39from pygments.util import ClassNotFound 

40 

41from rich.containers import Lines 

42from rich.padding import Padding, PaddingDimensions 

43 

44from ._loop import loop_first 

45from .cells import cell_len 

46from .color import Color, blend_rgb 

47from .console import Console, ConsoleOptions, JustifyMethod, RenderResult 

48from .jupyter import JupyterMixin 

49from .measure import Measurement 

50from .segment import Segment, Segments 

51from .style import Style, StyleType 

52from .text import Text 

53 

54TokenType = Tuple[str, ...] 

55 

56WINDOWS = sys.platform == "win32" 

57DEFAULT_THEME = "monokai" 

58 

59# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py 

60# A few modifications were made 

61 

62ANSI_LIGHT: Dict[TokenType, Style] = { 

63 Token: Style(), 

64 Whitespace: Style(color="white"), 

65 Comment: Style(dim=True), 

66 Comment.Preproc: Style(color="cyan"), 

67 Keyword: Style(color="blue"), 

68 Keyword.Type: Style(color="cyan"), 

69 Operator.Word: Style(color="magenta"), 

70 Name.Builtin: Style(color="cyan"), 

71 Name.Function: Style(color="green"), 

72 Name.Namespace: Style(color="cyan", underline=True), 

73 Name.Class: Style(color="green", underline=True), 

74 Name.Exception: Style(color="cyan"), 

75 Name.Decorator: Style(color="magenta", bold=True), 

76 Name.Variable: Style(color="red"), 

77 Name.Constant: Style(color="red"), 

78 Name.Attribute: Style(color="cyan"), 

79 Name.Tag: Style(color="bright_blue"), 

80 String: Style(color="yellow"), 

81 Number: Style(color="blue"), 

82 Generic.Deleted: Style(color="bright_red"), 

83 Generic.Inserted: Style(color="green"), 

84 Generic.Heading: Style(bold=True), 

85 Generic.Subheading: Style(color="magenta", bold=True), 

86 Generic.Prompt: Style(bold=True), 

87 Generic.Error: Style(color="bright_red"), 

88 Error: Style(color="red", underline=True), 

89} 

90 

91ANSI_DARK: Dict[TokenType, Style] = { 

92 Token: Style(), 

93 Whitespace: Style(color="bright_black"), 

94 Comment: Style(dim=True), 

95 Comment.Preproc: Style(color="bright_cyan"), 

96 Keyword: Style(color="bright_blue"), 

97 Keyword.Type: Style(color="bright_cyan"), 

98 Operator.Word: Style(color="bright_magenta"), 

99 Name.Builtin: Style(color="bright_cyan"), 

100 Name.Function: Style(color="bright_green"), 

101 Name.Namespace: Style(color="bright_cyan", underline=True), 

102 Name.Class: Style(color="bright_green", underline=True), 

103 Name.Exception: Style(color="bright_cyan"), 

104 Name.Decorator: Style(color="bright_magenta", bold=True), 

105 Name.Variable: Style(color="bright_red"), 

106 Name.Constant: Style(color="bright_red"), 

107 Name.Attribute: Style(color="bright_cyan"), 

108 Name.Tag: Style(color="bright_blue"), 

109 String: Style(color="yellow"), 

110 Number: Style(color="bright_blue"), 

111 Generic.Deleted: Style(color="bright_red"), 

112 Generic.Inserted: Style(color="bright_green"), 

113 Generic.Heading: Style(bold=True), 

114 Generic.Subheading: Style(color="bright_magenta", bold=True), 

115 Generic.Prompt: Style(bold=True), 

116 Generic.Error: Style(color="bright_red"), 

117 Error: Style(color="red", underline=True), 

118} 

119 

120RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK} 

121NUMBERS_COLUMN_DEFAULT_PADDING = 2 

122 

123 

124class SyntaxTheme(ABC): 

125 """Base class for a syntax theme.""" 

126 

127 @abstractmethod 

128 def get_style_for_token(self, token_type: TokenType) -> Style: 

129 """Get a style for a given Pygments token.""" 

130 raise NotImplementedError # pragma: no cover 

131 

132 @abstractmethod 

133 def get_background_style(self) -> Style: 

134 """Get the background color.""" 

135 raise NotImplementedError # pragma: no cover 

136 

137 

138class PygmentsSyntaxTheme(SyntaxTheme): 

139 """Syntax theme that delegates to Pygments theme.""" 

140 

141 def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None: 

142 self._style_cache: Dict[TokenType, Style] = {} 

143 if isinstance(theme, str): 

144 try: 

145 self._pygments_style_class = get_style_by_name(theme) 

146 except ClassNotFound: 

147 self._pygments_style_class = get_style_by_name("default") 

148 else: 

149 self._pygments_style_class = theme 

150 

151 self._background_color = self._pygments_style_class.background_color 

152 self._background_style = Style(bgcolor=self._background_color) 

153 

154 def get_style_for_token(self, token_type: TokenType) -> Style: 

155 """Get a style from a Pygments class.""" 

156 try: 

157 return self._style_cache[token_type] 

158 except KeyError: 

159 try: 

160 pygments_style = self._pygments_style_class.style_for_token(token_type) 

161 except KeyError: 

162 style = Style.null() 

163 else: 

164 color = pygments_style["color"] 

165 bgcolor = pygments_style["bgcolor"] 

166 style = Style( 

167 color="#" + color if color else "#000000", 

168 bgcolor="#" + bgcolor if bgcolor else self._background_color, 

169 bold=pygments_style["bold"], 

170 italic=pygments_style["italic"], 

171 underline=pygments_style["underline"], 

172 ) 

173 self._style_cache[token_type] = style 

174 return style 

175 

176 def get_background_style(self) -> Style: 

177 return self._background_style 

178 

179 

180class ANSISyntaxTheme(SyntaxTheme): 

181 """Syntax theme to use standard colors.""" 

182 

183 def __init__(self, style_map: Dict[TokenType, Style]) -> None: 

184 self.style_map = style_map 

185 self._missing_style = Style.null() 

186 self._background_style = Style.null() 

187 self._style_cache: Dict[TokenType, Style] = {} 

188 

189 def get_style_for_token(self, token_type: TokenType) -> Style: 

190 """Look up style in the style map.""" 

191 try: 

192 return self._style_cache[token_type] 

193 except KeyError: 

194 # Styles form a hierarchy 

195 # We need to go from most to least specific 

196 # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",) 

197 get_style = self.style_map.get 

198 token = tuple(token_type) 

199 style = self._missing_style 

200 while token: 

201 _style = get_style(token) 

202 if _style is not None: 

203 style = _style 

204 break 

205 token = token[:-1] 

206 self._style_cache[token_type] = style 

207 return style 

208 

209 def get_background_style(self) -> Style: 

210 return self._background_style 

211 

212 

213SyntaxPosition = Tuple[int, int] 

214 

215 

216class _SyntaxHighlightRange(NamedTuple): 

217 """ 

218 A range to highlight in a Syntax object. 

219 `start` and `end` are 2-integers tuples, where the first integer is the line number 

220 (starting from 1) and the second integer is the column index (starting from 0). 

221 """ 

222 

223 style: StyleType 

224 start: SyntaxPosition 

225 end: SyntaxPosition 

226 style_before: bool = False 

227 

228 

229class PaddingProperty: 

230 """Descriptor to get and set padding.""" 

231 

232 def __get__(self, obj: Syntax, objtype: Type[Syntax]) -> Tuple[int, int, int, int]: 

233 """Space around the Syntax.""" 

234 return obj._padding 

235 

236 def __set__(self, obj: Syntax, padding: PaddingDimensions) -> None: 

237 obj._padding = Padding.unpack(padding) 

238 

239 

240class Syntax(JupyterMixin): 

241 """Construct a Syntax object to render syntax highlighted code. 

242 

243 Args: 

244 code (str): Code to highlight. 

245 lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/) 

246 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai". 

247 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False. 

248 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. 

249 start_line (int, optional): Starting number for line numbers. Defaults to 1. 

250 line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render. 

251 A value of None in the tuple indicates the range is open in that direction. 

252 highlight_lines (Set[int]): A set of line numbers to highlight. 

253 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width. 

254 tab_size (int, optional): Size of tabs. Defaults to 4. 

255 word_wrap (bool, optional): Enable word wrapping. 

256 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None. 

257 indent_guides (bool, optional): Show indent guides. Defaults to False. 

258 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding). 

259 """ 

260 

261 _pygments_style_class: Type[PygmentsStyle] 

262 _theme: SyntaxTheme 

263 

264 @classmethod 

265 def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme: 

266 """Get a syntax theme instance.""" 

267 if isinstance(name, SyntaxTheme): 

268 return name 

269 theme: SyntaxTheme 

270 if name in RICH_SYNTAX_THEMES: 

271 theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name]) 

272 else: 

273 theme = PygmentsSyntaxTheme(name) 

274 return theme 

275 

276 def __init__( 

277 self, 

278 code: str, 

279 lexer: Union[Lexer, str], 

280 *, 

281 theme: Union[str, SyntaxTheme] = DEFAULT_THEME, 

282 dedent: bool = False, 

283 line_numbers: bool = False, 

284 start_line: int = 1, 

285 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None, 

286 highlight_lines: Optional[Set[int]] = None, 

287 code_width: Optional[int] = None, 

288 tab_size: int = 4, 

289 word_wrap: bool = False, 

290 background_color: Optional[str] = None, 

291 indent_guides: bool = False, 

292 padding: PaddingDimensions = 0, 

293 ) -> None: 

294 self.code = code 

295 self._lexer = lexer 

296 self.dedent = dedent 

297 self.line_numbers = line_numbers 

298 self.start_line = start_line 

299 self.line_range = line_range 

300 self.highlight_lines = highlight_lines or set() 

301 self.code_width = code_width 

302 self.tab_size = tab_size 

303 self.word_wrap = word_wrap 

304 self.background_color = background_color 

305 self.background_style = ( 

306 Style(bgcolor=background_color) if background_color else Style() 

307 ) 

308 self.indent_guides = indent_guides 

309 self._padding = Padding.unpack(padding) 

310 

311 self._theme = self.get_theme(theme) 

312 self._stylized_ranges: List[_SyntaxHighlightRange] = [] 

313 

314 padding = PaddingProperty() 

315 

316 @classmethod 

317 def from_path( 

318 cls, 

319 path: str, 

320 encoding: str = "utf-8", 

321 lexer: Optional[Union[Lexer, str]] = None, 

322 theme: Union[str, SyntaxTheme] = DEFAULT_THEME, 

323 dedent: bool = False, 

324 line_numbers: bool = False, 

325 line_range: Optional[Tuple[int, int]] = None, 

326 start_line: int = 1, 

327 highlight_lines: Optional[Set[int]] = None, 

328 code_width: Optional[int] = None, 

329 tab_size: int = 4, 

330 word_wrap: bool = False, 

331 background_color: Optional[str] = None, 

332 indent_guides: bool = False, 

333 padding: PaddingDimensions = 0, 

334 ) -> "Syntax": 

335 """Construct a Syntax object from a file. 

336 

337 Args: 

338 path (str): Path to file to highlight. 

339 encoding (str): Encoding of file. 

340 lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content. 

341 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs". 

342 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True. 

343 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. 

344 start_line (int, optional): Starting number for line numbers. Defaults to 1. 

345 line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render. 

346 highlight_lines (Set[int]): A set of line numbers to highlight. 

347 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width. 

348 tab_size (int, optional): Size of tabs. Defaults to 4. 

349 word_wrap (bool, optional): Enable word wrapping of code. 

350 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None. 

351 indent_guides (bool, optional): Show indent guides. Defaults to False. 

352 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding). 

353 

354 Returns: 

355 [Syntax]: A Syntax object that may be printed to the console 

356 """ 

357 code = Path(path).read_text(encoding=encoding) 

358 

359 if not lexer: 

360 lexer = cls.guess_lexer(path, code=code) 

361 

362 return cls( 

363 code, 

364 lexer, 

365 theme=theme, 

366 dedent=dedent, 

367 line_numbers=line_numbers, 

368 line_range=line_range, 

369 start_line=start_line, 

370 highlight_lines=highlight_lines, 

371 code_width=code_width, 

372 tab_size=tab_size, 

373 word_wrap=word_wrap, 

374 background_color=background_color, 

375 indent_guides=indent_guides, 

376 padding=padding, 

377 ) 

378 

379 @classmethod 

380 def guess_lexer(cls, path: str, code: Optional[str] = None) -> str: 

381 """Guess the alias of the Pygments lexer to use based on a path and an optional string of code. 

382 If code is supplied, it will use a combination of the code and the filename to determine the 

383 best lexer to use. For example, if the file is ``index.html`` and the file contains Django 

384 templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no 

385 templating language is used, the "html" lexer will be used. If no string of code 

386 is supplied, the lexer will be chosen based on the file extension.. 

387 

388 Args: 

389 path (AnyStr): The path to the file containing the code you wish to know the lexer for. 

390 code (str, optional): Optional string of code that will be used as a fallback if no lexer 

391 is found for the supplied path. 

392 

393 Returns: 

394 str: The name of the Pygments lexer that best matches the supplied path/code. 

395 """ 

396 lexer: Optional[Lexer] = None 

397 lexer_name = "default" 

398 if code: 

399 try: 

400 lexer = guess_lexer_for_filename(path, code) 

401 except ClassNotFound: 

402 pass 

403 

404 if not lexer: 

405 try: 

406 _, ext = os.path.splitext(path) 

407 if ext: 

408 extension = ext.lstrip(".").lower() 

409 lexer = get_lexer_by_name(extension) 

410 except ClassNotFound: 

411 pass 

412 

413 if lexer: 

414 if lexer.aliases: 

415 lexer_name = lexer.aliases[0] 

416 else: 

417 lexer_name = lexer.name 

418 

419 return lexer_name 

420 

421 def _get_base_style(self) -> Style: 

422 """Get the base style.""" 

423 default_style = self._theme.get_background_style() + self.background_style 

424 return default_style 

425 

426 def _get_token_color(self, token_type: TokenType) -> Optional[Color]: 

427 """Get a color (if any) for the given token. 

428 

429 Args: 

430 token_type (TokenType): A token type tuple from Pygments. 

431 

432 Returns: 

433 Optional[Color]: Color from theme, or None for no color. 

434 """ 

435 style = self._theme.get_style_for_token(token_type) 

436 return style.color 

437 

438 @property 

439 def lexer(self) -> Optional[Lexer]: 

440 """The lexer for this syntax, or None if no lexer was found. 

441 

442 Tries to find the lexer by name if a string was passed to the constructor. 

443 """ 

444 

445 if isinstance(self._lexer, Lexer): 

446 return self._lexer 

447 try: 

448 return get_lexer_by_name( 

449 self._lexer, 

450 stripnl=False, 

451 ensurenl=True, 

452 tabsize=self.tab_size, 

453 ) 

454 except ClassNotFound: 

455 return None 

456 

457 @property 

458 def default_lexer(self) -> Lexer: 

459 """A Pygments Lexer to use if one is not specified or invalid.""" 

460 return get_lexer_by_name( 

461 "text", 

462 stripnl=False, 

463 ensurenl=True, 

464 tabsize=self.tab_size, 

465 ) 

466 

467 def highlight( 

468 self, 

469 code: str, 

470 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None, 

471 ) -> Text: 

472 """Highlight code and return a Text instance. 

473 

474 Args: 

475 code (str): Code to highlight. 

476 line_range(Tuple[int, int], optional): Optional line range to highlight. 

477 

478 Returns: 

479 Text: A text instance containing highlighted syntax. 

480 """ 

481 

482 base_style = self._get_base_style() 

483 justify: JustifyMethod = ( 

484 "default" if base_style.transparent_background else "left" 

485 ) 

486 

487 text = Text( 

488 justify=justify, 

489 style=base_style, 

490 tab_size=self.tab_size, 

491 no_wrap=not self.word_wrap, 

492 ) 

493 _get_theme_style = self._theme.get_style_for_token 

494 

495 lexer = self.lexer or self.default_lexer 

496 

497 if lexer is None: 

498 text.append(code) 

499 else: 

500 if line_range: 

501 # More complicated path to only stylize a portion of the code 

502 # This speeds up further operations as there are less spans to process 

503 line_start, line_end = line_range 

504 

505 def line_tokenize() -> Iterable[Tuple[Any, str]]: 

506 """Split tokens to one per line.""" 

507 assert lexer # required to make MyPy happy - we know lexer is not None at this point 

508 

509 for token_type, token in lexer.get_tokens(code): 

510 while token: 

511 line_token, new_line, token = token.partition("\n") 

512 yield token_type, line_token + new_line 

513 

514 def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]: 

515 """Convert tokens to spans.""" 

516 tokens = iter(line_tokenize()) 

517 line_no = 0 

518 _line_start = line_start - 1 if line_start else 0 

519 

520 # Skip over tokens until line start 

521 while line_no < _line_start: 

522 try: 

523 _token_type, token = next(tokens) 

524 except StopIteration: 

525 break 

526 yield (token, None) 

527 if token.endswith("\n"): 

528 line_no += 1 

529 # Generate spans until line end 

530 for token_type, token in tokens: 

531 yield (token, _get_theme_style(token_type)) 

532 if token.endswith("\n"): 

533 line_no += 1 

534 if line_end and line_no >= line_end: 

535 break 

536 

537 text.append_tokens(tokens_to_spans()) 

538 

539 else: 

540 text.append_tokens( 

541 (token, _get_theme_style(token_type)) 

542 for token_type, token in lexer.get_tokens(code) 

543 ) 

544 if self.background_color is not None: 

545 text.stylize(f"on {self.background_color}") 

546 

547 if self._stylized_ranges: 

548 self._apply_stylized_ranges(text) 

549 

550 return text 

551 

552 def stylize_range( 

553 self, 

554 style: StyleType, 

555 start: SyntaxPosition, 

556 end: SyntaxPosition, 

557 style_before: bool = False, 

558 ) -> None: 

559 """ 

560 Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered. 

561 Line numbers are 1-based, while column indexes are 0-based. 

562 

563 Args: 

564 style (StyleType): The style to apply. 

565 start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`. 

566 end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`. 

567 style_before (bool): Apply the style before any existing styles. 

568 """ 

569 self._stylized_ranges.append( 

570 _SyntaxHighlightRange(style, start, end, style_before) 

571 ) 

572 

573 def _get_line_numbers_color(self, blend: float = 0.3) -> Color: 

574 background_style = self._theme.get_background_style() + self.background_style 

575 background_color = background_style.bgcolor 

576 if background_color is None or background_color.is_system_defined: 

577 return Color.default() 

578 foreground_color = self._get_token_color(Token.Text) 

579 if foreground_color is None or foreground_color.is_system_defined: 

580 return foreground_color or Color.default() 

581 new_color = blend_rgb( 

582 background_color.get_truecolor(), 

583 foreground_color.get_truecolor(), 

584 cross_fade=blend, 

585 ) 

586 return Color.from_triplet(new_color) 

587 

588 @property 

589 def _numbers_column_width(self) -> int: 

590 """Get the number of characters used to render the numbers column.""" 

591 column_width = 0 

592 if self.line_numbers: 

593 column_width = ( 

594 len(str(self.start_line + self.code.count("\n"))) 

595 + NUMBERS_COLUMN_DEFAULT_PADDING 

596 ) 

597 return column_width 

598 

599 def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]: 

600 """Get background, number, and highlight styles for line numbers.""" 

601 background_style = self._get_base_style() 

602 if background_style.transparent_background: 

603 return Style.null(), Style(dim=True), Style.null() 

604 if console.color_system in ("256", "truecolor"): 

605 number_style = Style.chain( 

606 background_style, 

607 self._theme.get_style_for_token(Token.Text), 

608 Style(color=self._get_line_numbers_color()), 

609 self.background_style, 

610 ) 

611 highlight_number_style = Style.chain( 

612 background_style, 

613 self._theme.get_style_for_token(Token.Text), 

614 Style(bold=True, color=self._get_line_numbers_color(0.9)), 

615 self.background_style, 

616 ) 

617 else: 

618 number_style = background_style + Style(dim=True) 

619 highlight_number_style = background_style + Style(dim=False) 

620 return background_style, number_style, highlight_number_style 

621 

622 def __rich_measure__( 

623 self, console: "Console", options: "ConsoleOptions" 

624 ) -> "Measurement": 

625 _, right, _, left = self.padding 

626 padding = left + right 

627 if self.code_width is not None: 

628 width = self.code_width + self._numbers_column_width + padding + 1 

629 return Measurement(self._numbers_column_width, width) 

630 lines = self.code.splitlines() 

631 width = ( 

632 self._numbers_column_width 

633 + padding 

634 + (max(cell_len(line) for line in lines) if lines else 0) 

635 ) 

636 if self.line_numbers: 

637 width += 1 

638 return Measurement(self._numbers_column_width, width) 

639 

640 def __rich_console__( 

641 self, console: Console, options: ConsoleOptions 

642 ) -> RenderResult: 

643 segments = Segments(self._get_syntax(console, options)) 

644 if any(self.padding): 

645 yield Padding(segments, style=self._get_base_style(), pad=self.padding) 

646 else: 

647 yield segments 

648 

649 def _get_syntax( 

650 self, 

651 console: Console, 

652 options: ConsoleOptions, 

653 ) -> Iterable[Segment]: 

654 """ 

655 Get the Segments for the Syntax object, excluding any vertical/horizontal padding 

656 """ 

657 transparent_background = self._get_base_style().transparent_background 

658 _pad_top, pad_right, _pad_bottom, pad_left = self.padding 

659 horizontal_padding = pad_left + pad_right 

660 code_width = ( 

661 ( 

662 (options.max_width - self._numbers_column_width - 1) 

663 if self.line_numbers 

664 else options.max_width 

665 ) 

666 - horizontal_padding 

667 if self.code_width is None 

668 else self.code_width 

669 ) 

670 code_width = max(0, code_width) 

671 

672 ends_on_nl, processed_code = self._process_code(self.code) 

673 text = self.highlight(processed_code, self.line_range) 

674 

675 if not self.line_numbers and not self.word_wrap and not self.line_range: 

676 if not ends_on_nl: 

677 text.remove_suffix("\n") 

678 # Simple case of just rendering text 

679 style = ( 

680 self._get_base_style() 

681 + self._theme.get_style_for_token(Comment) 

682 + Style(dim=True) 

683 + self.background_style 

684 ) 

685 if self.indent_guides and not options.ascii_only: 

686 text = text.with_indent_guides(self.tab_size, style=style) 

687 text.overflow = "crop" 

688 if style.transparent_background: 

689 yield from console.render( 

690 text, options=options.update(width=code_width) 

691 ) 

692 else: 

693 syntax_lines = console.render_lines( 

694 text, 

695 options.update(width=code_width, height=None, justify="left"), 

696 style=self.background_style, 

697 pad=True, 

698 new_lines=True, 

699 ) 

700 for syntax_line in syntax_lines: 

701 yield from syntax_line 

702 return 

703 

704 start_line, end_line = self.line_range or (None, None) 

705 line_offset = 0 

706 if start_line: 

707 line_offset = max(0, start_line - 1) 

708 lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl) 

709 if self.line_range: 

710 if line_offset > len(lines): 

711 return 

712 lines = lines[line_offset:end_line] 

713 

714 if self.indent_guides and not options.ascii_only: 

715 style = ( 

716 self._get_base_style() 

717 + self._theme.get_style_for_token(Comment) 

718 + Style(dim=True) 

719 + self.background_style 

720 ) 

721 lines = ( 

722 Text("\n") 

723 .join(lines) 

724 .with_indent_guides(self.tab_size, style=style + Style(italic=False)) 

725 .split("\n", allow_blank=True) 

726 ) 

727 

728 numbers_column_width = self._numbers_column_width 

729 render_options = options.update(width=code_width) 

730 

731 highlight_line = self.highlight_lines.__contains__ 

732 _Segment = Segment 

733 new_line = _Segment("\n") 

734 

735 line_pointer = "> " if options.legacy_windows else "❱ " 

736 

737 ( 

738 background_style, 

739 number_style, 

740 highlight_number_style, 

741 ) = self._get_number_styles(console) 

742 

743 for line_no, line in enumerate(lines, self.start_line + line_offset): 

744 if self.word_wrap: 

745 wrapped_lines = console.render_lines( 

746 line, 

747 render_options.update(height=None, justify="left"), 

748 style=background_style, 

749 pad=not transparent_background, 

750 ) 

751 else: 

752 segments = list(line.render(console, end="")) 

753 if options.no_wrap: 

754 wrapped_lines = [segments] 

755 else: 

756 wrapped_lines = [ 

757 _Segment.adjust_line_length( 

758 segments, 

759 render_options.max_width, 

760 style=background_style, 

761 pad=not transparent_background, 

762 ) 

763 ] 

764 

765 if self.line_numbers: 

766 wrapped_line_left_pad = _Segment( 

767 " " * numbers_column_width + " ", background_style 

768 ) 

769 for first, wrapped_line in loop_first(wrapped_lines): 

770 if first: 

771 line_column = str(line_no).rjust(numbers_column_width - 2) + " " 

772 if highlight_line(line_no): 

773 yield _Segment(line_pointer, Style(color="red")) 

774 yield _Segment(line_column, highlight_number_style) 

775 else: 

776 yield _Segment(" ", highlight_number_style) 

777 yield _Segment(line_column, number_style) 

778 else: 

779 yield wrapped_line_left_pad 

780 yield from wrapped_line 

781 yield new_line 

782 else: 

783 for wrapped_line in wrapped_lines: 

784 yield from wrapped_line 

785 yield new_line 

786 

787 def _apply_stylized_ranges(self, text: Text) -> None: 

788 """ 

789 Apply stylized ranges to a text instance, 

790 using the given code to determine the right portion to apply the style to. 

791 

792 Args: 

793 text (Text): Text instance to apply the style to. 

794 """ 

795 code = text.plain 

796 newlines_offsets = [ 

797 # Let's add outer boundaries at each side of the list: 

798 0, 

799 # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z": 

800 *[ 

801 match.start() + 1 

802 for match in re.finditer("\n", code, flags=re.MULTILINE) 

803 ], 

804 len(code) + 1, 

805 ] 

806 

807 for stylized_range in self._stylized_ranges: 

808 start = _get_code_index_for_syntax_position( 

809 newlines_offsets, stylized_range.start 

810 ) 

811 end = _get_code_index_for_syntax_position( 

812 newlines_offsets, stylized_range.end 

813 ) 

814 if start is not None and end is not None: 

815 if stylized_range.style_before: 

816 text.stylize_before(stylized_range.style, start, end) 

817 else: 

818 text.stylize(stylized_range.style, start, end) 

819 

820 def _process_code(self, code: str) -> Tuple[bool, str]: 

821 """ 

822 Applies various processing to a raw code string 

823 (normalises it so it always ends with a line return, dedents it if necessary, etc.) 

824 

825 Args: 

826 code (str): The raw code string to process 

827 

828 Returns: 

829 Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return, 

830 while the string is the processed code. 

831 """ 

832 ends_on_nl = code.endswith("\n") 

833 processed_code = code if ends_on_nl else code + "\n" 

834 processed_code = ( 

835 textwrap.dedent(processed_code) if self.dedent else processed_code 

836 ) 

837 processed_code = processed_code.expandtabs(self.tab_size) 

838 return ends_on_nl, processed_code 

839 

840 

841def _get_code_index_for_syntax_position( 

842 newlines_offsets: Sequence[int], position: SyntaxPosition 

843) -> Optional[int]: 

844 """ 

845 Returns the index of the code string for the given positions. 

846 

847 Args: 

848 newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet. 

849 position (SyntaxPosition): The position to search for. 

850 

851 Returns: 

852 Optional[int]: The index of the code string for this position, or `None` 

853 if the given position's line number is out of range (if it's the column that is out of range 

854 we silently clamp its value so that it reaches the end of the line) 

855 """ 

856 lines_count = len(newlines_offsets) 

857 

858 line_number, column_index = position 

859 if line_number > lines_count or len(newlines_offsets) < (line_number + 1): 

860 return None # `line_number` is out of range 

861 line_index = line_number - 1 

862 line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1 

863 # If `column_index` is out of range: let's silently clamp it: 

864 column_index = min(line_length, column_index) 

865 return newlines_offsets[line_index] + column_index 

866 

867 

868if __name__ == "__main__": # pragma: no cover 

869 import argparse 

870 import sys 

871 

872 parser = argparse.ArgumentParser( 

873 description="Render syntax to the console with Rich" 

874 ) 

875 parser.add_argument( 

876 "path", 

877 metavar="PATH", 

878 help="path to file, or - for stdin", 

879 ) 

880 parser.add_argument( 

881 "-c", 

882 "--force-color", 

883 dest="force_color", 

884 action="store_true", 

885 default=None, 

886 help="force color for non-terminals", 

887 ) 

888 parser.add_argument( 

889 "-i", 

890 "--indent-guides", 

891 dest="indent_guides", 

892 action="store_true", 

893 default=False, 

894 help="display indent guides", 

895 ) 

896 parser.add_argument( 

897 "-l", 

898 "--line-numbers", 

899 dest="line_numbers", 

900 action="store_true", 

901 help="render line numbers", 

902 ) 

903 parser.add_argument( 

904 "-w", 

905 "--width", 

906 type=int, 

907 dest="width", 

908 default=None, 

909 help="width of output (default will auto-detect)", 

910 ) 

911 parser.add_argument( 

912 "-r", 

913 "--wrap", 

914 dest="word_wrap", 

915 action="store_true", 

916 default=False, 

917 help="word wrap long lines", 

918 ) 

919 parser.add_argument( 

920 "-s", 

921 "--soft-wrap", 

922 action="store_true", 

923 dest="soft_wrap", 

924 default=False, 

925 help="enable soft wrapping mode", 

926 ) 

927 parser.add_argument( 

928 "-t", "--theme", dest="theme", default="monokai", help="pygments theme" 

929 ) 

930 parser.add_argument( 

931 "-b", 

932 "--background-color", 

933 dest="background_color", 

934 default=None, 

935 help="Override background color", 

936 ) 

937 parser.add_argument( 

938 "-x", 

939 "--lexer", 

940 default=None, 

941 dest="lexer_name", 

942 help="Lexer name", 

943 ) 

944 parser.add_argument( 

945 "-p", "--padding", type=int, default=0, dest="padding", help="Padding" 

946 ) 

947 parser.add_argument( 

948 "--highlight-line", 

949 type=int, 

950 default=None, 

951 dest="highlight_line", 

952 help="The line number (not index!) to highlight", 

953 ) 

954 args = parser.parse_args() 

955 

956 from rich.console import Console 

957 

958 console = Console(force_terminal=args.force_color, width=args.width) 

959 

960 if args.path == "-": 

961 code = sys.stdin.read() 

962 syntax = Syntax( 

963 code=code, 

964 lexer=args.lexer_name, 

965 line_numbers=args.line_numbers, 

966 word_wrap=args.word_wrap, 

967 theme=args.theme, 

968 background_color=args.background_color, 

969 indent_guides=args.indent_guides, 

970 padding=args.padding, 

971 highlight_lines={args.highlight_line}, 

972 ) 

973 else: 

974 syntax = Syntax.from_path( 

975 args.path, 

976 lexer=args.lexer_name, 

977 line_numbers=args.line_numbers, 

978 word_wrap=args.word_wrap, 

979 theme=args.theme, 

980 background_color=args.background_color, 

981 indent_guides=args.indent_guides, 

982 padding=args.padding, 

983 highlight_lines={args.highlight_line}, 

984 ) 

985 console.print(syntax, soft_wrap=args.soft_wrap)