|
| 1 | +# Copyright The OpenTelemetry Authors |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import difflib |
| 16 | +import os |
| 17 | + |
| 18 | + |
| 19 | +class VisualDiffer: |
| 20 | + """Colorize differential outputs, which can be useful in development of |
| 21 | + semantic conventions |
| 22 | + """ |
| 23 | + |
| 24 | + @staticmethod |
| 25 | + def colorize_text(r: int, g: int, b: int, text: str): |
| 26 | + """ |
| 27 | + Colorize text according to ANSI standards |
| 28 | + The way this works is we send out a control character, |
| 29 | + then send the color information (the r,g,b parts), |
| 30 | + then the normal text, then an escape char to end the coloring |
| 31 | +
|
| 32 | + ## Breakdown of magic values |
| 33 | + 33 (octal) == 1b (hexadecimal) == ESC control character |
| 34 | +
|
| 35 | + ESC[38 => This sets foreground color |
| 36 | + https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters |
| 37 | +
|
| 38 | + ESC[38;2; => Set foreground color with 24-bit color mode |
| 39 | + https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit |
| 40 | +
|
| 41 | + ESC[0m => Resets foreground color (basically turn off "coloring mode") |
| 42 | +
|
| 43 | + {r};{g};{b};m => Sets the color mode. "m" denotes the end of the escape sequence prefix |
| 44 | +
|
| 45 | + For more information and colors, see |
| 46 | + https://en.wikipedia.org/wiki/ANSI_escape_code#Colors |
| 47 | + """ |
| 48 | + escape_color_24bitmode = "\x1b[38;2" |
| 49 | + reset_color = "\x1b[0m" |
| 50 | + return f"{escape_color_24bitmode};{r};{g};{b}m{text}{reset_color}" |
| 51 | + |
| 52 | + @classmethod |
| 53 | + def removed(cls, text: str) -> str: |
| 54 | + return cls.colorize_text(255, 0, 0, text) |
| 55 | + |
| 56 | + @classmethod |
| 57 | + def added(cls, text: str) -> str: |
| 58 | + return cls.colorize_text(0, 255, 0, text) |
| 59 | + |
| 60 | + @classmethod |
| 61 | + def visual_diff(cls, a: str, b: str): |
| 62 | + """ |
| 63 | + Prints git-like colored diff using ANSI terminal coloring. |
| 64 | + Diff is "from a to b", that is, red text is text deleted in `a` |
| 65 | + while green text is new to `b` |
| 66 | + """ |
| 67 | + if "true" != os.environ.get("COLORED_DIFF", "false").lower(): |
| 68 | + return "".join(difflib.context_diff(a, b)) |
| 69 | + |
| 70 | + colored_diff = [] |
| 71 | + diff_partitions = difflib.SequenceMatcher(None, a, b) |
| 72 | + for operation, a_start, a_end, b_start, b_end in diff_partitions.get_opcodes(): |
| 73 | + if operation == "equal": |
| 74 | + colored_diff.append(a[a_start:a_end]) |
| 75 | + elif operation == "insert": |
| 76 | + colored_diff.append(cls.added(b[b_start:b_end])) |
| 77 | + elif operation == "delete": |
| 78 | + colored_diff.append(cls.removed(a[a_start:a_end])) |
| 79 | + elif operation == "replace": |
| 80 | + colored_diff.append(cls.added(b[b_start:b_end])) |
| 81 | + colored_diff.append(cls.removed(a[a_start:a_end])) |
| 82 | + else: |
| 83 | + # Log.warn would be best here |
| 84 | + raise ValueError( |
| 85 | + f"Unhandled opcode from difflib in semantic conversion markdown renderer: {operation}" |
| 86 | + ) |
| 87 | + return "".join(colored_diff) |
0 commit comments