diff --git a/examples/ansi.css b/examples/ansi.css new file mode 100644 index 00000000..7c8c93b3 --- /dev/null +++ b/examples/ansi.css @@ -0,0 +1,31 @@ +.ansi-black-bg { background-color: #3e424d; } +.ansi-red-bg { background-color: #e75c58; } +.ansi-green-bg { background-color: #00a250; } +.ansi-yellow-bg { background-color: #ddb62b; } +.ansi-blue-bg { background-color: #208ffb; } +.ansi-magenta-bg { background-color: #d160c4; } +.ansi-cyan-bg { background-color: #60c6c8; } +.ansi-white-bg { background-color: #c5c1b4; } +.ansi-black-intense-fg { color: #282c36; } +.ansi-red-intense-fg { color: #b22b31; } +.ansi-green-intense-fg { color: #007427; } +.ansi-yellow-intense-fg { color: #b27d12; } +.ansi-blue-intense-fg { color: #0065ca; } +.ansi-magenta-intense-fg { color: #a03196; } +.ansi-cyan-intense-fg { color: #258f8f; } +.ansi-white-intense-fg { color: #a1a6b2; } +.ansi-black-intense-bg { background-color: #282c36; } +.ansi-red-intense-bg { background-color: #b22b31; } +.ansi-green-intense-bg { background-color: #007427; } +.ansi-yellow-intense-bg { background-color: #b27d12; } +.ansi-blue-intense-bg { background-color: #0065ca; } +.ansi-magenta-intense-bg { background-color: #a03196; } +.ansi-cyan-intense-bg { background-color: #258f8f; } +.ansi-white-intense-bg { background-color: #a1a6b2; } +.ansi-bold { font-weight: bold; } +.ansi-underline { text-decoration: underline; } +{ background-color: #a03196; } +.ansi-cyan-intense-bg { background-color: #258f8f; } +.ansi-white-intense-bg { background-color: #a1a6b2; } +.ansi-bold { font-weight: bold; } +.ansi-underline { text-decoration: underline; } diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 79773e7c..3dc01417 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -6,6 +6,7 @@ 'git_url': 'https://github.com/fastai/fastcore/', 'lib_path': 'fastcore'}, 'syms': { 'fastcore.all': {}, + 'fastcore.ansi': {}, 'fastcore.basics': { 'fastcore.basics.AttrDict': ('basics.html#attrdict', 'fastcore/basics.py'), 'fastcore.basics.AttrDict.__dir__': ('basics.html#attrdict.__dir__', 'fastcore/basics.py'), 'fastcore.basics.AttrDict.__getattr__': ('basics.html#attrdict.__getattr__', 'fastcore/basics.py'), diff --git a/fastcore/ansi.py b/fastcore/ansi.py new file mode 100644 index 00000000..216756d5 --- /dev/null +++ b/fastcore/ansi.py @@ -0,0 +1,180 @@ +"Filters for processing ANSI colors." + +# Copyright (c) IPython Development Team. +# Modifications by Jeremy Howard. + +import re, markupsafe + +__all__ = ["strip_ansi", "ansi2html", "ansi2latex"] + +_ANSI_RE = re.compile("\x1b\\[(.*?)([@-~])") +_ANSI_COLORS = ( "ansi-black", "ansi-red", "ansi-green", "ansi-yellow", "ansi-blue", "ansi-magenta", "ansi-cyan", "ansi-white", "ansi-black-intense", + "ansi-red-intense", "ansi-green-intense", "ansi-yellow-intense", "ansi-blue-intense", "ansi-magenta-intense", "ansi-cyan-intense", "ansi-white-intense") + + +def strip_ansi(source): + "Remove ANSI escape codes from text." + return _ANSI_RE.sub("", source) + + +def ansi2html(text): + "Convert ANSI colors to HTML colors." + text = markupsafe.escape(text) + return _ansi2anything(text, _htmlconverter) + + +def ansi2latex(text): + "Convert ANSI colors to LaTeX colors." + return _ansi2anything(text, _latexconverter) + + +def _htmlconverter(fg, bg, bold, underline, inverse): + "Return start and end tags for given foreground/background/bold/underline." + if (fg, bg, bold, underline, inverse) == (None, None, False, False, False): return "", "" + + classes,styles = [],[] + if inverse: fg, bg = bg, fg + if isinstance(fg, int): classes.append(_ANSI_COLORS[fg] + "-fg") + elif fg: styles.append("color: rgb({},{},{})".format(*fg)) + elif inverse: classes.append("ansi-default-inverse-fg") + + if isinstance(bg, int): classes.append(_ANSI_COLORS[bg] + "-bg") + elif bg: styles.append("background-color: rgb({},{},{})".format(*bg)) + elif inverse: classes.append("ansi-default-inverse-bg") + + if bold: classes.append("ansi-bold") + if underline: classes.append("ansi-underline") + + starttag = "" + + +def _latexconverter(fg, bg, bold, underline, inverse): + "Return start and end markup given foreground/background/bold/underline." + if (fg, bg, bold, underline, inverse) == (None, None, False, False, False): return "", "" + starttag, endtag = "", "" + if inverse: fg, bg = bg, fg + if isinstance(fg, int): + starttag += r"\textcolor{" + _ANSI_COLORS[fg] + "}{" + endtag = "}" + endtag + elif fg: + # See http://tex.stackexchange.com/a/291102/13684 + starttag += r"\def\tcRGB{\textcolor[RGB]}\expandafter" + starttag += r"\tcRGB\expandafter{{\detokenize{{{},{},{}}}}}{{".format(*fg) + endtag = "}" + endtag + elif inverse: + starttag += r"\textcolor{ansi-default-inverse-fg}{" + endtag = "}" + endtag + + if isinstance(bg, int): + starttag += r"\setlength{\fboxsep}{0pt}" + starttag += r"\colorbox{" + _ANSI_COLORS[bg] + "}{" + endtag = r"\strut}" + endtag + elif bg: + starttag += r"\setlength{\fboxsep}{0pt}" + # See http://tex.stackexchange.com/a/291102/13684 + starttag += r"\def\cbRGB{\colorbox[RGB]}\expandafter" + starttag += r"\cbRGB\expandafter{{\detokenize{{{},{},{}}}}}{{".format(*bg) + endtag = r"\strut}" + endtag + elif inverse: + starttag += r"\setlength{\fboxsep}{0pt}" + starttag += r"\colorbox{ansi-default-inverse-bg}{" + endtag = r"\strut}" + endtag + + if bold: + starttag += r"\textbf{" + endtag = "}" + endtag + + if underline: + starttag += r"\underline{" + endtag = "}" + endtag + + return starttag, endtag + + +def _ansi2anything(text, converter): + "Convert ANSI colors to HTML or LaTeX." + fg, bg = None, None + bold, underline, inverse = False, False, False + numbers,out = [],[] + + while text: + m = _ANSI_RE.search(text) + if m: + if m.group(2) == "m": + # Empty code is same as code 0 + try: numbers = [int(n) if n else 0 for n in m.group(1).split(";")] + except ValueError: pass # Invalid color specification + else: pass # Not a color code + chunk, text = text[: m.start()], text[m.end() :] + else: chunk, text = text, "" + + if chunk: + starttag, endtag = converter( + fg + 8 if bold and fg in range(8) else fg, # type:ignore[operator] + bg, bold, underline, inverse) + out.append(starttag) + out.append(chunk) + out.append(endtag) + + while numbers: + n = numbers.pop(0) + if n == 0: + # Code 0 (same as empty code): reset everything + fg = bg = None + bold = underline = inverse = False + elif n == 1: bold = True + elif n == 4: underline = True + # Code 5: blinking + elif n == 5: bold = True + elif n == 7: inverse = True + elif n in (21, 22): bold = False + elif n == 24: underline = False + elif n == 27: inverse = False + elif 30 <= n <= 37: fg = n - 30 + elif n == 38: + try: fg = _get_extended_color(numbers) + except ValueError: numbers.clear() + elif n == 39: fg = None + elif 40 <= n <= 47: bg = n - 40 + elif n == 48: + try: bg = _get_extended_color(numbers) + except ValueError: numbers.clear() + elif n == 49: bg = None + elif 90 <= n <= 97: fg = n - 90 + 8 + elif 100 <= n <= 107: bg = n - 100 + 8 + else: pass # Unknown codes are ignored + return "".join(out) + + +def _get_extended_color(numbers): + n = numbers.pop(0) + if n == 2 and len(numbers) >= 3: + # 24-bit RGB + r = numbers.pop(0) + g = numbers.pop(0) + b = numbers.pop(0) + if not all(0 <= c <= 255 for c in (r, g, b)): raise ValueError() + elif n == 5 and len(numbers) >= 1: + # 256 colors + idx = numbers.pop(0) + if idx < 0: raise ValueError() + # 16 default terminal colors + if idx < 16: return idx + if idx < 232: + # 6x6x6 color cube, see http://stackoverflow.com/a/27165165/500098 + r = (idx - 16) // 36 + r = 55 + r * 40 if r > 0 else 0 + g = ((idx - 16) % 36) // 6 + g = 55 + g * 40 if g > 0 else 0 + b = (idx - 16) % 6 + b = 55 + b * 40 if b > 0 else 0 + # grayscale, see http://stackoverflow.com/a/27165165/500098 + elif idx < 256: r = g = b = (idx - 232) * 10 + 8 + else: raise ValueError() + else: raise ValueError() + return r, g, b + diff --git a/nbs/13_external.ipynb b/nbs/13_external.ipynb new file mode 100644 index 00000000..2f666bbc --- /dev/null +++ b/nbs/13_external.ipynb @@ -0,0 +1,236 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "84424ee0", + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "from nbdev.showdoc import *\n", + "from fastcore.utils import *\n", + "from IPython.display import HTML" + ] + }, + { + "cell_type": "markdown", + "id": "38f5d4d8", + "metadata": {}, + "source": [ + "# External modules" + ] + }, + { + "cell_type": "markdown", + "id": "917d045d", + "metadata": {}, + "source": [ + "fastcore includes functionality from some modules from other projects that have been copied here, in cases where the original is no longer maintained, or where the original includes dependencies that we'd rather avoid." + ] + }, + { + "cell_type": "markdown", + "id": "6de4cd08", + "metadata": {}, + "source": [ + "## imghdr" + ] + }, + { + "cell_type": "markdown", + "id": "da14c2f4", + "metadata": {}, + "source": [ + "fastcore includes a copy of the Python standard library's `imghdr` module, which was deprecated in Python 3.11, and removed in 3.13. However since it's still widely used (including within fastcore), we are providing it here. We have also added some fixes to the automatic detection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d24fc389", + "metadata": {}, + "outputs": [], + "source": [ + "from fastcore.imghdr import what,tests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "912911b4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'jpeg'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "what('images/puppy.jpg')" + ] + }, + { + "cell_type": "markdown", + "id": "a48ef98e", + "metadata": {}, + "source": [ + "These are the tests provided:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a17b5a9b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "test_jpeg, test_png, test_gif, test_tiff, test_rgb, test_pbm, test_pgm, test_ppm, test_rast, test_xbm, test_bmp, test_webp, test_exr\n" + ] + } + ], + "source": [ + "print(', '.join(t.__name__ for t in tests))" + ] + }, + { + "cell_type": "markdown", + "id": "13fda770", + "metadata": {}, + "source": [ + "## ansi" + ] + }, + { + "cell_type": "markdown", + "id": "2f18f18d", + "metadata": {}, + "source": [ + "[nbconvert](https://github.com/jupyter/nbconvert/blob/main/nbconvert/filters/ansi.py) provides handy functionality to convert ansi terminal codes to HTML, which we've copied to fastcore so they can be used without nbconvert's prequisites. Also nbconvert doesn't document them, so we're showing some examples here." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d346cd0f", + "metadata": {}, + "outputs": [], + "source": [ + "from fastcore.ansi import ansi2html,strip_ansi" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "877e0f15", + "metadata": {}, + "outputs": [], + "source": [ + "ansi_test = \"\"\"\\x1b[0;31m---------------------------------------------------------------------------\\x1b[0m\n", + "\\x1b[0;31mZeroDivisionError\\x1b[0m Traceback (most recent call last)\n", + "File \\x1b[0;32m:1\\x1b[0m\n", + "\\x1b[0;32m----> 1\\x1b[0m \\x1b[38;5;241m1\\x1b[39m\\x1b[38;5;241m/\\x1b[39m\\x1b[38;5;241m0\\x1b[39m\n", + "\n", + "\\x1b[0;31mZeroDivisionError\\x1b[0m: division by zero\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9deb6835", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "---------------------------------------------------------------------------\n", + "ZeroDivisionError Traceback (most recent call last)\n", + "File <input-1>:1\n", + "----> 1 1/0\n", + "\n", + "ZeroDivisionError: division by zero\n" + ] + } + ], + "source": [ + "out = ansi2html(ansi_test)\n", + "print(out)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57d28b0a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
File <input-1>:1
----> 1 1/0
ZeroDivisionError: division by zero
"
+      ],
+      "text/plain": [
+       ""
+      ]
+     },
+     "execution_count": null,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "test_err = ''.join([f\"
{o}
\" for o in out.splitlines()])\n", + "HTML(f'
{test_err}
')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "93895b9e",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "---------------------------------------------------------------------------\n",
+      "ZeroDivisionError                         Traceback (most recent call last)\n",
+      "File :1\n",
+      "----> 1 1/0\n",
+      "\n",
+      "ZeroDivisionError: division by zero\n"
+     ]
+    }
+   ],
+   "source": [
+    "print(strip_ansi(ansi_tests))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ce817290",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "python3",
+   "language": "python",
+   "name": "python3"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}