jsonschema-diff 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jsonschema_diff/__init__.py +17 -0
- jsonschema_diff/cli.py +190 -0
- jsonschema_diff/color/__init__.py +12 -0
- jsonschema_diff/color/abstraction.py +62 -0
- jsonschema_diff/color/base.py +124 -0
- jsonschema_diff/color/stages/__init__.py +31 -0
- jsonschema_diff/color/stages/mono_lines.py +112 -0
- jsonschema_diff/color/stages/path.py +191 -0
- jsonschema_diff/color/stages/replace.py +184 -0
- jsonschema_diff/config_maker.py +100 -0
- jsonschema_diff/core/__init__.py +7 -0
- jsonschema_diff/core/abstraction.py +51 -0
- jsonschema_diff/core/config.py +100 -0
- jsonschema_diff/core/custom_compare/__init__.py +7 -0
- jsonschema_diff/core/custom_compare/list.py +98 -0
- jsonschema_diff/core/custom_compare/range.py +241 -0
- jsonschema_diff/core/parameter_base.py +118 -0
- jsonschema_diff/core/parameter_combined.py +36 -0
- jsonschema_diff/core/property.py +243 -0
- jsonschema_diff/core/tools/__init__.py +6 -0
- jsonschema_diff/core/tools/combine.py +105 -0
- jsonschema_diff/core/tools/compare.py +77 -0
- jsonschema_diff/core/tools/context.py +127 -0
- jsonschema_diff/core/tools/render.py +128 -0
- jsonschema_diff/pypi_interface.py +187 -0
- jsonschema_diff/sphinx/__init__.py +32 -0
- jsonschema_diff/sphinx/directive.py +136 -0
- jsonschema_diff/table_render.py +227 -0
- jsonschema_diff-0.1.0.dist-info/METADATA +292 -0
- jsonschema_diff-0.1.0.dist-info/RECORD +34 -0
- jsonschema_diff-0.1.0.dist-info/WHEEL +5 -0
- jsonschema_diff-0.1.0.dist-info/entry_points.txt +2 -0
- jsonschema_diff-0.1.0.dist-info/licenses/LICENSE +21 -0
- jsonschema_diff-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from importlib import import_module
|
|
2
|
+
from types import ModuleType
|
|
3
|
+
|
|
4
|
+
from .config_maker import ConfigMaker
|
|
5
|
+
from .pypi_interface import JsonSchemaDiff
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Lazy-import подмодуля sphinx (тянет Rich → Sphinx только по требованию)
|
|
9
|
+
def __getattr__(name: str) -> ModuleType: # pragma: no cover
|
|
10
|
+
if name == "sphinx":
|
|
11
|
+
return import_module("jsonschema_diff.sphinx")
|
|
12
|
+
raise AttributeError(name)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = ["JsonSchemaDiff", "ConfigMaker"]
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
jsonschema_diff/cli.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
jsonschema_diff CLI
|
|
3
|
+
===================
|
|
4
|
+
|
|
5
|
+
A tiny command-line front-end around :py:mod:`jsonschema_diff`
|
|
6
|
+
that highlights semantic differences between two JSON-Schema
|
|
7
|
+
documents directly in your terminal.
|
|
8
|
+
|
|
9
|
+
Typical usage
|
|
10
|
+
-------------
|
|
11
|
+
>>> jsonschema-diff old.schema.json new.schema.json
|
|
12
|
+
>>> jsonschema-diff --no-color --legend old.json new.json
|
|
13
|
+
>>> jsonschema-diff --exit-code old.json new.json # useful in CI
|
|
14
|
+
|
|
15
|
+
Exit status
|
|
16
|
+
-----------
|
|
17
|
+
* **0** – the two schemas are semantically identical
|
|
18
|
+
* **1** – at least one difference was detected (only when
|
|
19
|
+
``--exit-code`` is given)
|
|
20
|
+
|
|
21
|
+
The CLI is intentionally minimal: *all* comparison options are taken
|
|
22
|
+
from :pyclass:`jsonschema_diff.ConfigMaker`, so the behaviour stays
|
|
23
|
+
in sync with the library defaults.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
from jsonschema_diff import ConfigMaker, JsonSchemaDiff
|
|
34
|
+
from jsonschema_diff.color import HighlighterPipeline
|
|
35
|
+
from jsonschema_diff.color.stages import (
|
|
36
|
+
MonoLinesHighlighter,
|
|
37
|
+
PathHighlighter,
|
|
38
|
+
ReplaceGenericHighlighter,
|
|
39
|
+
)
|
|
40
|
+
from jsonschema_diff.core.parameter_base import Compare
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _make_highlighter(disable_color: bool) -> HighlighterPipeline:
|
|
44
|
+
"""
|
|
45
|
+
Create the high-lighting pipeline used to colorise diff output.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
disable_color :
|
|
50
|
+
When *True* ANSI escape sequences are suppressed even if the
|
|
51
|
+
invoking TTY advertises color support (e.g. when piping the
|
|
52
|
+
output into a file).
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
HighlighterPipeline
|
|
57
|
+
Either an **empty** pipeline (no colour) or the standard
|
|
58
|
+
three-stage pipeline consisting of
|
|
59
|
+
:class:`~jsonschema_diff.color.stages.MonoLinesHighlighter`,
|
|
60
|
+
:class:`~jsonschema_diff.color.stages.ReplaceGenericHighlighter`
|
|
61
|
+
and :class:`~jsonschema_diff.color.stages.PathHighlighter`.
|
|
62
|
+
|
|
63
|
+
Note
|
|
64
|
+
-----
|
|
65
|
+
The composition of the *default* pipeline mirrors what the core
|
|
66
|
+
library exposes; duplicating the stages here keeps the CLI fully
|
|
67
|
+
self-contained while allowing future customisation.
|
|
68
|
+
|
|
69
|
+
Examples
|
|
70
|
+
--------
|
|
71
|
+
>>> _make_highlighter(True)
|
|
72
|
+
HighlighterPipeline(stages=[])
|
|
73
|
+
>>> _make_highlighter(False).stages # doctest: +ELLIPSIS
|
|
74
|
+
[<jsonschema_diff.color.stages.MonoLinesHighlighter ...>, ...]
|
|
75
|
+
"""
|
|
76
|
+
if disable_color:
|
|
77
|
+
return HighlighterPipeline([])
|
|
78
|
+
return HighlighterPipeline(
|
|
79
|
+
[
|
|
80
|
+
MonoLinesHighlighter(),
|
|
81
|
+
ReplaceGenericHighlighter(),
|
|
82
|
+
PathHighlighter(),
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
88
|
+
"""
|
|
89
|
+
Construct the :pyclass:`argparse.ArgumentParser` for the CLI.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
argparse.ArgumentParser
|
|
94
|
+
The fully configured parser containing positional arguments
|
|
95
|
+
for the *old* and *new* schema paths, together with three
|
|
96
|
+
optional feature flags.
|
|
97
|
+
|
|
98
|
+
See Also
|
|
99
|
+
--------
|
|
100
|
+
* :pyfunc:`main` – where the parser is consumed.
|
|
101
|
+
* The *argparse* documentation for available formatting options.
|
|
102
|
+
"""
|
|
103
|
+
p = argparse.ArgumentParser(
|
|
104
|
+
prog="jsonschema-diff",
|
|
105
|
+
description="Show the difference between two JSON-Schema files",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Positional arguments
|
|
109
|
+
p.add_argument("old_schema", help="Path to the *old* schema")
|
|
110
|
+
p.add_argument("new_schema", help="Path to the *new* schema")
|
|
111
|
+
|
|
112
|
+
# Output options
|
|
113
|
+
p.add_argument(
|
|
114
|
+
"--no-color",
|
|
115
|
+
action="store_true",
|
|
116
|
+
help="Disable ANSI colors even if the terminal supports them",
|
|
117
|
+
)
|
|
118
|
+
p.add_argument(
|
|
119
|
+
"--legend",
|
|
120
|
+
action="store_true",
|
|
121
|
+
help="Print a legend explaining diff symbols at the end",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Exit-code control
|
|
125
|
+
p.add_argument(
|
|
126
|
+
"--exit-code",
|
|
127
|
+
action="store_true",
|
|
128
|
+
help="Return **1** if differences are detected, otherwise **0**",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return p
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main(argv: list[str] | None = None) -> None: # pragma: no cover
|
|
135
|
+
"""
|
|
136
|
+
CLI entry-point (invoked by ``python -m jsonschema_diff`` or by the
|
|
137
|
+
``jsonschema-diff`` console script).
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
argv :
|
|
142
|
+
Command-line argument vector **excluding** the executable name.
|
|
143
|
+
When *None* (default) ``sys.argv[1:]`` is used – this is the
|
|
144
|
+
behaviour required by *setuptools* console-scripts.
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
Note
|
|
148
|
+
----
|
|
149
|
+
The function performs four sequential steps:
|
|
150
|
+
|
|
151
|
+
1. Build a :class:`JsonSchemaDiff` instance.
|
|
152
|
+
2. Compare the two user-supplied schema files.
|
|
153
|
+
3. Print a colourised diff (optionally with a legend).
|
|
154
|
+
4. Optionally exit with code 1 if differences are present.
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
args = _build_parser().parse_args(argv)
|
|
158
|
+
|
|
159
|
+
# 1. Build the wrapper object
|
|
160
|
+
diff = JsonSchemaDiff(
|
|
161
|
+
config=ConfigMaker.make(),
|
|
162
|
+
colorize_pipeline=_make_highlighter(args.no_color),
|
|
163
|
+
legend_ignore=[Compare], # as in the library example
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def try_load(data: str) -> dict | str:
|
|
167
|
+
try:
|
|
168
|
+
return dict(json.loads(data))
|
|
169
|
+
except json.JSONDecodeError:
|
|
170
|
+
return str(data)
|
|
171
|
+
|
|
172
|
+
# 2. Compare the files
|
|
173
|
+
diff.compare(
|
|
174
|
+
old_schema=try_load(args.old_schema),
|
|
175
|
+
new_schema=try_load(args.new_schema),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# 3. Print the result
|
|
179
|
+
diff.print(
|
|
180
|
+
with_legend=args.legend,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# 4. Optional special exit code
|
|
184
|
+
if args.exit_code:
|
|
185
|
+
# ``last_compare_list`` is filled during render/print.
|
|
186
|
+
sys.exit(1 if diff.last_compare_list else 0)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__": # pragma: no cover
|
|
190
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convenience re-exports for the colour sub-package.
|
|
3
|
+
|
|
4
|
+
End-users can simply write::
|
|
5
|
+
|
|
6
|
+
from jsonschema_diff.color import HighlighterPipeline, LineHighlighter
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .abstraction import LineHighlighter
|
|
10
|
+
from .base import HighlighterPipeline
|
|
11
|
+
|
|
12
|
+
__all__ = ["HighlighterPipeline", "LineHighlighter"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import List, Protocol, Sequence
|
|
2
|
+
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Abstraction for line-based high-lighters
|
|
7
|
+
=======================================
|
|
8
|
+
|
|
9
|
+
This module defines :class:`LineHighlighter`, a thin
|
|
10
|
+
:class:`typing.Protocol` that specifies the minimal contract required by the
|
|
11
|
+
colour pipeline implemented in :pyfile:`base.py`.
|
|
12
|
+
|
|
13
|
+
Implementors are expected to decorate :class:`rich.text.Text` objects **in
|
|
14
|
+
place**; therefore methods must not alter the underlying string content –
|
|
15
|
+
only styling metadata.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LineHighlighter(Protocol):
|
|
20
|
+
"""Protocol for single-line high-lighters.
|
|
21
|
+
|
|
22
|
+
Concrete implementations *may* also override
|
|
23
|
+
:meth:`colorize_lines` for bulk operations, but
|
|
24
|
+
:meth:`colorize_line` is the only mandatory method.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def colorize_line(self, line: Text) -> Text:
|
|
28
|
+
"""Stylise one line **in-place** and return it.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
line :
|
|
33
|
+
A single :class:`rich.text.Text` instance to be colour-styled.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
rich.text.Text
|
|
38
|
+
The **same** `Text` object, now containing style spans.
|
|
39
|
+
|
|
40
|
+
Raises
|
|
41
|
+
------
|
|
42
|
+
NotImplementedError
|
|
43
|
+
Always here; concrete subclasses must override this method.
|
|
44
|
+
"""
|
|
45
|
+
raise NotImplementedError("LineHighlighter.colorize_line должен быть переопределен")
|
|
46
|
+
|
|
47
|
+
def colorize_lines(self, lines: Sequence[Text]) -> List[Text]:
|
|
48
|
+
"""Vectorised helper that stylises a *sequence* of lines.
|
|
49
|
+
|
|
50
|
+
The naïve fallback simply delegates to :meth:`colorize_line`.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
lines :
|
|
55
|
+
Ordered collection of :class:`rich.text.Text` objects.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
list[rich.text.Text]
|
|
60
|
+
The original objects, now styled in place.
|
|
61
|
+
"""
|
|
62
|
+
return [self.colorize_line(t) for t in lines]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Composable *Rich*-native colouring pipeline
|
|
5
|
+
==========================================
|
|
6
|
+
|
|
7
|
+
Provides :class:`HighlighterPipeline`, an orchestrator that feeds raw
|
|
8
|
+
multi-line strings through a chain of :class:`LineHighlighter` stages.
|
|
9
|
+
Each stage operates on :class:`rich.text.Text` so it can add style spans
|
|
10
|
+
without mutating the text itself.
|
|
11
|
+
|
|
12
|
+
Typical usage
|
|
13
|
+
-------------
|
|
14
|
+
|
|
15
|
+
>>> pipeline = HighlighterPipeline([MySyntaxHL(), MyDiffHL()])
|
|
16
|
+
>>> ansi_output = pipeline.colorize_and_render(src_string)
|
|
17
|
+
print(ansi_output)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING, Iterable
|
|
21
|
+
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
26
|
+
from .abstraction import LineHighlighter # noqa: F401 (imported for typing only)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class 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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Built-in high-lighter stages
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
This *sub-package* bundles three ready-to-use implementations of
|
|
6
|
+
:class:`jsonschema_diff.color.abstraction.LineHighlighter` that cover the most
|
|
7
|
+
common needs when rendering a JSON-Schema diff to the terminal:
|
|
8
|
+
|
|
9
|
+
* :class:`MonoLinesHighlighter` – apply a foreground colour chosen from a
|
|
10
|
+
*prefix → colour* mapping
|
|
11
|
+
* :class:`ReplaceGenericHighlighter` – highlight token-level changes within
|
|
12
|
+
``OLD -> NEW`` tails
|
|
13
|
+
* :class:`PathHighlighter` – pretty-print JSON-Pointer-like paths
|
|
14
|
+
|
|
15
|
+
Importing the package directly re-exports these classes so you can write
|
|
16
|
+
succinct code such as ::
|
|
17
|
+
|
|
18
|
+
from jsonschema_diff.color.stages import MonoLinesHighlighter
|
|
19
|
+
|
|
20
|
+
Only the three public classes below are exported via :py:data:`__all__`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .mono_lines import MonoLinesHighlighter
|
|
24
|
+
from .path import PathHighlighter
|
|
25
|
+
from .replace import ReplaceGenericHighlighter
|
|
26
|
+
|
|
27
|
+
__all__: list[str] = [
|
|
28
|
+
"MonoLinesHighlighter",
|
|
29
|
+
"ReplaceGenericHighlighter",
|
|
30
|
+
"PathHighlighter",
|
|
31
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Monochrome prefix-based high-lighter
|
|
5
|
+
====================================
|
|
6
|
+
|
|
7
|
+
A :class:`~jsonschema_diff.color.abstraction.LineHighlighter` that decorates a
|
|
8
|
+
single :class:`rich.text.Text` *in-place* by looking at its **first matching
|
|
9
|
+
prefix**—typically the leading character produced by *unified diff* output
|
|
10
|
+
(``-``, ``+``, *etc.*).
|
|
11
|
+
|
|
12
|
+
Why a “Rich-native” rewrite?
|
|
13
|
+
----------------------------
|
|
14
|
+
The original *jsonschema-diff* implementation rendered to a string containing
|
|
15
|
+
ANSI escape codes. In interactive TUI applications you often want to keep the
|
|
16
|
+
object as a real ``Text`` so you can:
|
|
17
|
+
|
|
18
|
+
* put it into a :class:`rich.table.Table`,
|
|
19
|
+
* display it inside a :class:`rich.panel.Panel`,
|
|
20
|
+
* or update it live without re-parsing ANSI.
|
|
21
|
+
|
|
22
|
+
This drop-in replacement keeps behaviour identical while removing the ANSI
|
|
23
|
+
round-trip.
|
|
24
|
+
"""
|
|
25
|
+
from typing import Mapping, Optional
|
|
26
|
+
|
|
27
|
+
from rich.style import Style
|
|
28
|
+
from rich.text import Text
|
|
29
|
+
|
|
30
|
+
from ..abstraction import LineHighlighter
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MonoLinesHighlighter(LineHighlighter):
|
|
34
|
+
"""Colourise one line based on a prefix lookup.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
bold :
|
|
39
|
+
Apply the *bold* attribute together with the foreground colour.
|
|
40
|
+
Enabled by default to keep parity with the original.
|
|
41
|
+
default_color :
|
|
42
|
+
Fallback colour when no rule matches. If *None* the line is left
|
|
43
|
+
unchanged (except for *bold* when that option is *True*).
|
|
44
|
+
case_sensitive :
|
|
45
|
+
Whether prefix matching should be case-sensitive. Defaults to *False*.
|
|
46
|
+
rules :
|
|
47
|
+
Mapping ``prefix → colour``. The first match wins; order is therefore
|
|
48
|
+
significant. If *None*, the following defaults are used::
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
"-": "red",
|
|
52
|
+
"+": "green",
|
|
53
|
+
"r": "cyan",
|
|
54
|
+
"m": "cyan",
|
|
55
|
+
}
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
bold: bool = True,
|
|
61
|
+
default_color: Optional[str] = None,
|
|
62
|
+
case_sensitive: bool = False,
|
|
63
|
+
rules: Mapping[str, str] | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
if rules is None:
|
|
66
|
+
rules = {
|
|
67
|
+
"-": "red",
|
|
68
|
+
"+": "green",
|
|
69
|
+
"r": "cyan",
|
|
70
|
+
"m": "cyan",
|
|
71
|
+
}
|
|
72
|
+
self.bold = bold
|
|
73
|
+
self.default_color = default_color
|
|
74
|
+
self.case_sensitive = case_sensitive
|
|
75
|
+
self.rules: Mapping[str, str] = dict(rules) # preserve order
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Public API
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
def colorize_line(self, line: Text) -> Text:
|
|
81
|
+
"""Apply a single style pass **in place**.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
line :
|
|
86
|
+
The :class:`rich.text.Text` instance to be modified.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
rich.text.Text
|
|
91
|
+
**The very same instance** that was passed in—allowing fluent,
|
|
92
|
+
chainable APIs.
|
|
93
|
+
|
|
94
|
+
Note
|
|
95
|
+
----
|
|
96
|
+
Only the *first* matching prefix is honoured; subsequent rules are
|
|
97
|
+
ignored, mirroring classic *grep* / *sed* behaviour.
|
|
98
|
+
"""
|
|
99
|
+
probe = line.plain if self.case_sensitive else line.plain.lower()
|
|
100
|
+
|
|
101
|
+
for prefix, color in self.rules.items():
|
|
102
|
+
pref = prefix if self.case_sensitive else prefix.lower()
|
|
103
|
+
if probe.startswith(pref):
|
|
104
|
+
line.stylize(Style(color=color, bold=self.bold), 0, len(line))
|
|
105
|
+
return line # first match wins
|
|
106
|
+
|
|
107
|
+
# --- fall-back -------------------------------------------------
|
|
108
|
+
if self.default_color is not None:
|
|
109
|
+
line.stylize(Style(color=self.default_color, bold=self.bold), 0, len(line))
|
|
110
|
+
elif self.bold:
|
|
111
|
+
line.stylize(Style(bold=True), 0, len(line))
|
|
112
|
+
return line
|