Coverage for jsonschema_diff/color/base.py: 38%

29 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2025-08-25 07:00 +0000

1from __future__ import annotations 

2 

3""" 

4Composable *Rich*-native colouring pipeline 

5========================================== 

6 

7Provides :class:`HighlighterPipeline`, an orchestrator that feeds raw 

8multi-line strings through a chain of :class:`LineHighlighter` stages. 

9Each stage operates on :class:`rich.text.Text` so it can add style spans 

10without mutating the text itself. 

11 

12Typical usage 

13------------- 

14 

15>>> pipeline = HighlighterPipeline([MySyntaxHL(), MyDiffHL()]) 

16>>> ansi_output = pipeline.colorize_and_render(src_string) 

17print(ansi_output) 

18""" 

19 

20from typing import TYPE_CHECKING, Iterable 

21 

22from rich.console import Console 

23from rich.text import Text 

24 

25if TYPE_CHECKING: # pragma: no cover 

26 from .abstraction import LineHighlighter # noqa: F401 (imported for typing only) 

27 

28 

29class HighlighterPipeline: # noqa: D101 

30 """Chain of :pyclass:`LineHighlighter` stages. 

31 

32 Parameters 

33 ---------- 

34 stages : 

35 Iterable of :class:`LineHighlighter` instances. The iterable is 

36 immediately materialised into a list so the pipeline can be reused. 

37 

38 Note 

39 ---- 

40 * Each input line is passed through **every** stage in order. 

41 * If a stage exposes a bulk :pyfunc:`colorize_lines` method it is 

42 preferred over per-line iteration for performance. 

43 """ 

44 

45 def __init__(self, stages: Iterable["LineHighlighter"]): 

46 self.stages: list["LineHighlighter"] = list(stages) 

47 

48 # ------------------------------------------------------------------ 

49 # Public helpers 

50 # ------------------------------------------------------------------ 

51 def colorize(self, text: str) -> Text: # noqa: D401 

52 """Return a rich ``Text`` object with all styles applied. 

53 

54 Parameters 

55 ---------- 

56 text : 

57 Multi-line string to be colourised. 

58 

59 Returns 

60 ------- 

61 One composite `Text` built by joining all styled lines with 

62 ``\\n`` separators. 

63 """ 

64 lines = text.splitlines() 

65 rich_lines = [Text(line) for line in lines] 

66 

67 for stage in self.stages: 

68 colorize_lines = getattr(stage, "colorize_lines", None) 

69 if callable(colorize_lines): 

70 colorize_lines(rich_lines) 

71 else: 

72 for rl in rich_lines: 

73 stage.colorize_line(rl) 

74 return Text("\n").join(rich_lines) 

75 

76 def colorize_and_render(self, text: str) -> str: 

77 """Colourise and immediately render to ANSI. 

78 

79 Parameters 

80 ---------- 

81 text : 

82 Multi-line input string. 

83 

84 Returns 

85 ------- 

86 ANSI-encoded string ready for terminal output. 

87 """ 

88 rich_lines = self.colorize(text) 

89 

90 console = Console( 

91 force_terminal=True, 

92 color_system="truecolor", 

93 width=self._detect_width(), 

94 legacy_windows=False, 

95 ) 

96 with console.capture() as cap: 

97 console.print(rich_lines, end="") 

98 return cap.get() 

99 

100 # ------------------------------------------------------------------ 

101 # Internal helpers 

102 # ------------------------------------------------------------------ 

103 @staticmethod 

104 def _detect_width(default: int = 512) -> int: # noqa: D401 

105 """Best-effort terminal width detection. 

106 

107 Falls back to *default* when a real TTY is not present 

108 (e.g. in CI). 

109 

110 Parameters 

111 ---------- 

112 default : 

113 Width to use when detection fails. Defaults to ``512``. 

114 

115 Returns 

116 ------- 

117 Column count deemed safe for rendering. 

118 """ 

119 try: 

120 from shutil import get_terminal_size 

121 

122 return max(get_terminal_size().columns, 20) 

123 except Exception: # pragma: no cover 

124 return default