From db049f6537c9f24c6f82b0ffddbc88ddd0de16be Mon Sep 17 00:00:00 2001 From: seem Date: Wed, 10 Aug 2022 17:02:01 +1000 Subject: [PATCH] Add `fastcore.style`: fast styling for friendly CLIs --- .gitignore | 1 + fastcore/_modidx.py | 7 +- fastcore/style.py | 69 +++++++ nbs/10_style.ipynb | 471 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 fastcore/style.py create mode 100644 nbs/10_style.ipynb diff --git a/.gitignore b/.gitignore index 97a2663f..5f699464 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,4 @@ checklink/cookies.txt # .gitconfig is now autogenerated .gitconfig +_docs diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 8f574179..44a27b7b 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -31,7 +31,7 @@ 'title': 'fastcore', 'tst_flags': '', 'user': 'fastai', - 'version': '1.5.16'}, + 'version': '1.5.17'}, 'syms': { 'fastcore.all': {}, 'fastcore.basics': { 'fastcore.basics.AttrDict': 'https://fastcore.fast.ai/basics.html#attrdict', 'fastcore.basics.AttrDict.copy': 'https://fastcore.fast.ai/basics.html#attrdict.copy', @@ -361,6 +361,11 @@ 'fastcore.shutil.disk_usage': 'https://fastcore.fast.ai/shutil.html#disk_usage', 'fastcore.shutil.move': 'https://fastcore.fast.ai/shutil.html#move', 'fastcore.shutil.rmtree': 'https://fastcore.fast.ai/shutil.html#rmtree'}, + 'fastcore.style': { 'fastcore.style.S': 'https://fastcore.fast.ai/style.html#s', + 'fastcore.style.Style': 'https://fastcore.fast.ai/style.html#style', + 'fastcore.style.StyleCode': 'https://fastcore.fast.ai/style.html#stylecode', + 'fastcore.style.demo': 'https://fastcore.fast.ai/style.html#demo', + 'fastcore.style.style_codes': 'https://fastcore.fast.ai/style.html#style_codes'}, 'fastcore.test': { 'fastcore.test.ExceptionExpected': 'https://fastcore.fast.ai/test.html#exceptionexpected', 'fastcore.test.TEST_IMAGE': 'https://fastcore.fast.ai/test.html#test_image', 'fastcore.test.TEST_IMAGE_BW': 'https://fastcore.fast.ai/test.html#test_image_bw', diff --git a/fastcore/style.py b/fastcore/style.py new file mode 100644 index 00000000..65112ffd --- /dev/null +++ b/fastcore/style.py @@ -0,0 +1,69 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/10_style.ipynb. + +# %% auto 0 +__all__ = ['style_codes', 'S', 'StyleCode', 'Style', 'demo'] + +# %% ../nbs/10_style.ipynb 3 +# Source: https://misc.flogisoft.com/bash/tip_colors_and_formatting +_base = 'red green yellow blue magenta cyan' +_regular = f'black {_base} light_gray' +_intense = 'dark_gray ' + ' '.join('light_'+o for o in _base.split()) + ' white' +_fmt = dict(bold=1,dim=2,underline=4,reverse=7,hidden=8) + +# %% ../nbs/10_style.ipynb 4 +class StyleCode: + "An escape sequence for styling terminal text." + def __init__(self, name, code, typ): self.name,self.code,self.typ = name,code,typ + def __str__(self): return f'\033[{self.code}m' + +# %% ../nbs/10_style.ipynb 7 +def _mk_codes(s, start, typ, fmt=None, **kwargs): + d = {k:i for i,k in enumerate(s.split())} if isinstance(s, str) else s + res = {k if fmt is None else fmt.format(k):start+v for k,v in d.items()} + res.update(kwargs) + return {k:StyleCode(k,v,typ) for k,v in res.items()} + +# %% ../nbs/10_style.ipynb 8 +# Hardcode `reset_bold=22` since 21 is not always supported +# See: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797 +style_codes = {**_mk_codes(_regular, 30, 'fg', default=39), + **_mk_codes(_intense, 90, 'fg'), + **_mk_codes(_regular, 40, 'bg', '{}_bg', default_bg=49), + **_mk_codes(_intense, 100, 'bg', '{}_bg'), + **_mk_codes(_fmt, 0, 'fmt'), + **_mk_codes(_fmt, 21, 'reset', 'reset_{}', reset=0, reset_bold=22)} + +# %% ../nbs/10_style.ipynb 9 +def _reset_code(s): + if s.typ == 'fg': return style_codes['default'] + if s.typ == 'bg': return style_codes['default_bg'] + if s.typ == 'fmt': return style_codes['reset_'+s.name] + +# %% ../nbs/10_style.ipynb 10 +class Style: + "A minimal terminal text styler." + def __init__(self, codes=None): self.codes = [] if codes is None else codes + def __dir__(self): return style_codes.keys() + def __getattr__(self, k): return Style(self.codes+[style_codes[k]]) + def __call__(self, obj): + set_ = ''.join(str(o) for o in self.codes) + reset = ''.join('' if o is None else str(o) for o in set(_reset_code(o) for o in self.codes)) + return set_ + str(obj) + reset + def __repr__(self): + nm = type(self).__name__ + res = f'<{nm}: ' + res += ' '.join(o.name for o in self.codes) if self.codes else 'none' + return res+'>' + +# %% ../nbs/10_style.ipynb 12 +S = Style() + +# %% ../nbs/10_style.ipynb 25 +def _demo(name, code): + s = getattr(S,name) + print(s(f'{code.code:>3} {name:16}')) + +# %% ../nbs/10_style.ipynb 26 +def demo(): + "Demonstrate all available styles and their codes." + for k,v in style_codes.items(): _demo(k,v) diff --git a/nbs/10_style.ipynb b/nbs/10_style.ipynb new file mode 100644 index 00000000..0c566ec9 --- /dev/null +++ b/nbs/10_style.ipynb @@ -0,0 +1,471 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "d0c84aa5", + "metadata": {}, + "outputs": [], + "source": [ + "#|default_exp style" + ] + }, + { + "cell_type": "markdown", + "id": "61bf8203", + "metadata": {}, + "source": [ + "# Style\n", + "\n", + "> Fast styling for friendly CLIs." + ] + }, + { + "cell_type": "markdown", + "id": "5437d2fc", + "metadata": {}, + "source": [ + "::: {.callout-note}\n", + "\n", + "Styled outputs don't show in Quarto documentation. Please use a notebook editor to correctly view this page.\n", + "\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "059709e3", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "# Source: https://misc.flogisoft.com/bash/tip_colors_and_formatting\n", + "_base = 'red green yellow blue magenta cyan'\n", + "_regular = f'black {_base} light_gray'\n", + "_intense = 'dark_gray ' + ' '.join('light_'+o for o in _base.split()) + ' white'\n", + "_fmt = dict(bold=1,dim=2,underline=4,reverse=7,hidden=8)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12d1cb35", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "class StyleCode:\n", + " \"An escape sequence for styling terminal text.\"\n", + " def __init__(self, name, code, typ): self.name,self.code,self.typ = name,code,typ\n", + " def __str__(self): return f'\\033[{self.code}m'" + ] + }, + { + "cell_type": "markdown", + "id": "84cac46c", + "metadata": {}, + "source": [ + "The primary building block of the `S` API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33671ebf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[34mhello\u001b[39m world\n" + ] + } + ], + "source": [ + "print(str(StyleCode('blue', 34, 'fg')) + 'hello' + str(StyleCode('default', 39, 'fg')) + ' world')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a35abf7", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _mk_codes(s, start, typ, fmt=None, **kwargs):\n", + " d = {k:i for i,k in enumerate(s.split())} if isinstance(s, str) else s\n", + " res = {k if fmt is None else fmt.format(k):start+v for k,v in d.items()}\n", + " res.update(kwargs)\n", + " return {k:StyleCode(k,v,typ) for k,v in res.items()}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ff754020", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "# Hardcode `reset_bold=22` since 21 is not always supported\n", + "# See: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797\n", + "style_codes = {**_mk_codes(_regular, 30, 'fg', default=39),\n", + " **_mk_codes(_intense, 90, 'fg'),\n", + " **_mk_codes(_regular, 40, 'bg', '{}_bg', default_bg=49),\n", + " **_mk_codes(_intense, 100, 'bg', '{}_bg'),\n", + " **_mk_codes(_fmt, 0, 'fmt'),\n", + " **_mk_codes(_fmt, 21, 'reset', 'reset_{}', reset=0, reset_bold=22)}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34a19083", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _reset_code(s):\n", + " if s.typ == 'fg': return style_codes['default']\n", + " if s.typ == 'bg': return style_codes['default_bg']\n", + " if s.typ == 'fmt': return style_codes['reset_'+s.name]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb561c7e", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "class Style:\n", + " \"A minimal terminal text styler.\"\n", + " def __init__(self, codes=None): self.codes = [] if codes is None else codes\n", + " def __dir__(self): return style_codes.keys()\n", + " def __getattr__(self, k): return Style(self.codes+[style_codes[k]])\n", + " def __call__(self, obj):\n", + " set_ = ''.join(str(o) for o in self.codes)\n", + " reset = ''.join('' if o is None else str(o) for o in set(_reset_code(o) for o in self.codes))\n", + " return set_ + str(obj) + reset\n", + " def __repr__(self):\n", + " nm = type(self).__name__\n", + " res = f'<{nm}: '\n", + " res += ' '.join(o.name for o in self.codes) if self.codes else 'none'\n", + " return res+'>'" + ] + }, + { + "cell_type": "markdown", + "id": "56848b63", + "metadata": {}, + "source": [ + "The main way to use it is via the exported `S` object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73c4d89c", + "metadata": {}, + "outputs": [], + "source": [ + "#|exports\n", + "S = Style()" + ] + }, + { + "cell_type": "markdown", + "id": "5bc1c8bf", + "metadata": {}, + "source": [ + "We start with an empty style:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2eb8de72", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "S" + ] + }, + { + "cell_type": "markdown", + "id": "de2fd3a8", + "metadata": {}, + "source": [ + "Define a new style by chaining attributes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3add7ec", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "s = S.blue.bold.underline\n", + "s" + ] + }, + { + "cell_type": "markdown", + "id": "45578ec4", + "metadata": {}, + "source": [ + "You can see a full list of available styles with auto-complete by typing S . Tab." + ] + }, + { + "cell_type": "markdown", + "id": "ed24db31", + "metadata": {}, + "source": [ + "Apply a style by calling it with a string:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59e5ab51", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'\\x1b[34m\\x1b[1m\\x1b[4mhello world\\x1b[21m\\x1b[39m\\x1b[24m'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "s('hello world')" + ] + }, + { + "cell_type": "markdown", + "id": "4fa4b809", + "metadata": {}, + "source": [ + "That's a raw string with the underlying escape sequences that tell the terminal how to format text. To see the styled version we have to print it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c5a8a96", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[34m\u001b[1m\u001b[4mhello world\u001b[21m\u001b[39m\u001b[24m\n" + ] + } + ], + "source": [ + "print(s('hello world'))" + ] + }, + { + "cell_type": "markdown", + "id": "9919519b", + "metadata": {}, + "source": [ + "You can also nest styles:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "653d867f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[1m\u001b[34mkey\u001b[39m = value \u001b[21m\u001b[37m \u001b[4m# With a comment\u001b[24m\u001b[39m and unstyled text\n" + ] + } + ], + "source": [ + "print(S.bold(S.blue('key') + ' = value ') + S.light_gray(' ' + S.underline('# With a comment')) + ' and unstyled text')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce59dc19", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[34mthis \u001b[1mis\u001b[21m a test\u001b[39m\n" + ] + } + ], + "source": [ + "print(S.blue('this '+S.bold('is')+' a test'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c016111", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _demo(name, code):\n", + " s = getattr(S,name)\n", + " print(s(f'{code.code:>3} {name:16}'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf21a9bd", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def demo():\n", + " \"Demonstrate all available styles and their codes.\"\n", + " for k,v in style_codes.items(): _demo(k,v)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe52f7cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[30m 30 black \u001b[39m\n", + "\u001b[31m 31 red \u001b[39m\n", + "\u001b[32m 32 green \u001b[39m\n", + "\u001b[33m 33 yellow \u001b[39m\n", + "\u001b[34m 34 blue \u001b[39m\n", + "\u001b[35m 35 magenta \u001b[39m\n", + "\u001b[36m 36 cyan \u001b[39m\n", + "\u001b[37m 37 light_gray \u001b[39m\n", + "\u001b[39m 39 default \u001b[39m\n", + "\u001b[90m 90 dark_gray \u001b[39m\n", + "\u001b[91m 91 light_red \u001b[39m\n", + "\u001b[92m 92 light_green \u001b[39m\n", + "\u001b[93m 93 light_yellow \u001b[39m\n", + "\u001b[94m 94 light_blue \u001b[39m\n", + "\u001b[95m 95 light_magenta \u001b[39m\n", + "\u001b[96m 96 light_cyan \u001b[39m\n", + "\u001b[97m 97 white \u001b[39m\n", + "\u001b[40m 40 black_bg \u001b[49m\n", + "\u001b[41m 41 red_bg \u001b[49m\n", + "\u001b[42m 42 green_bg \u001b[49m\n", + "\u001b[43m 43 yellow_bg \u001b[49m\n", + "\u001b[44m 44 blue_bg \u001b[49m\n", + "\u001b[45m 45 magenta_bg \u001b[49m\n", + "\u001b[46m 46 cyan_bg \u001b[49m\n", + "\u001b[47m 47 light_gray_bg \u001b[49m\n", + "\u001b[49m 49 default_bg \u001b[49m\n", + "\u001b[100m100 dark_gray_bg \u001b[49m\n", + "\u001b[101m101 light_red_bg \u001b[49m\n", + "\u001b[102m102 light_green_bg \u001b[49m\n", + "\u001b[103m103 light_yellow_bg \u001b[49m\n", + "\u001b[104m104 light_blue_bg \u001b[49m\n", + "\u001b[105m105 light_magenta_bg\u001b[49m\n", + "\u001b[106m106 light_cyan_bg \u001b[49m\n", + "\u001b[107m107 white_bg \u001b[49m\n", + "\u001b[1m 1 bold \u001b[21m\n", + "\u001b[2m 2 dim \u001b[22m\n", + "\u001b[4m 4 underline \u001b[24m\n", + "\u001b[7m 7 reverse \u001b[27m\n", + "\u001b[8m 8 hidden \u001b[28m\n", + "\u001b[21m 21 reset_bold \n", + "\u001b[22m 22 reset_dim \n", + "\u001b[24m 24 reset_underline \n", + "\u001b[27m 27 reset_reverse \n", + "\u001b[28m 28 reset_hidden \n", + "\u001b[0m 0 reset \n" + ] + } + ], + "source": [ + "demo()" + ] + }, + { + "cell_type": "markdown", + "id": "df973d4e", + "metadata": {}, + "source": [ + "# Export -" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad32b076", + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "import nbdev; nbdev.nbdev_export()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84fe290c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}