From 31325b7ed10d22b31269fd07758bbf7a4b693802 Mon Sep 17 00:00:00 2001 From: Salehbigdeli Date: Wed, 28 Sep 2022 05:28:42 +0200 Subject: [PATCH 001/182] Enhancement add ability to have *args in scripts function --- fastcore/docments.py | 9 +++-- fastcore/script.py | 17 ++++++---- nbs/06_docments.ipynb | 79 ++++++++++++++++++++++++++++++++----------- nbs/08_script.ipynb | 57 ++++++++++++++++++++++++++++--- 4 files changed, 126 insertions(+), 36 deletions(-) diff --git a/fastcore/docments.py b/fastcore/docments.py index d5935605..5ab4fd21 100644 --- a/fastcore/docments.py +++ b/fastcore/docments.py @@ -91,9 +91,9 @@ def _get_comment(line, arg, comments, parms): line -= 1 return dedent('\n'.join(reversed(res))) if res else None -def _get_full(anno, name, default, docs): +def _get_full(anno, name, default, docs, kind): if anno==empty and default!=empty: anno = type(default) - return AttrDict(docment=docs.get(name), anno=anno, default=default) + return AttrDict(docment=docs.get(name), anno=anno, default=default, kind=kind) # %% ../nbs/06_docments.ipynb 22 def _merge_doc(dm, npdoc): @@ -142,8 +142,8 @@ def _docments(s, returns=True, eval_str=False): if isinstance(s,str): s = eval(s) sig = signature(s) - res = {arg:_get_full(p.annotation, p.name, p.default, docs) for arg,p in sig.parameters.items()} - if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs) + res = {arg:_get_full(p.annotation, p.name, p.default, docs, p.kind) for arg,p in sig.parameters.items()} + if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs, '') res = _merge_docs(res, nps) if eval_str: hints = type_hints(s) @@ -158,7 +158,6 @@ def docments(elt, full=False, **kwargs): r = {} params = set(signature(elt).parameters) params.add('return') - def _update_docments(f, r): if hasattr(f, '__delwrap__'): _update_docments(f.__delwrap__, r) r.update({k:v for k,v in _docments(f, **kwargs).items() if k in params diff --git a/fastcore/script.py b/fastcore/script.py index cb545791..40204c75 100644 --- a/fastcore/script.py +++ b/fastcore/script.py @@ -10,6 +10,7 @@ from .imports import * from .utils import * from .docments import docments +from inspect import Parameter # %% ../nbs/08_script.ipynb 15 def store_true(): @@ -50,7 +51,6 @@ def set_default(self, d): else: self.default = d if self.default is not None: self.help += f" (default: {self.default})" - @property def pre(self): return '--' if self.opt else '' @property @@ -78,12 +78,13 @@ def anno_parser(func, # Function to get arguments from param = v.anno if not isinstance(param,Param): param = Param(v.docment, v.anno) param.set_default(v.default) - p.add_argument(f"{param.pre}{k}", **param.kwargs) + if getattr(v, 'kind', '') == Parameter.VAR_POSITIONAL: p.add_argument(f"{param.pre}{k}",**param.kwargs, nargs='*') + else: p.add_argument(f"{param.pre}{k}", **param.kwargs) p.add_argument(f"--pdb", help=argparse.SUPPRESS, action='store_true') p.add_argument(f"--xtra", help=argparse.SUPPRESS, type=str) return p -# %% ../nbs/08_script.ipynb 34 +# %% ../nbs/08_script.ipynb 36 def args_from_prog(func, prog): "Extract args from `prog`" if prog is None or '#' not in prog: return {} @@ -96,10 +97,10 @@ def args_from_prog(func, prog): if t: args[k] = t(v) return args -# %% ../nbs/08_script.ipynb 37 +# %% ../nbs/08_script.ipynb 39 SCRIPT_INFO = SimpleNamespace(func=None) -# %% ../nbs/08_script.ipynb 39 +# %% ../nbs/08_script.ipynb 41 def call_parse(func=None, nested=False): "Decorator to create a simple CLI from `func` using `anno_parser`" if func is None: return partial(call_parse, nested=nested) @@ -113,10 +114,12 @@ def _f(*args, **kwargs): p = anno_parser(func) if nested: args, sys.argv[1:] = p.parse_known_args() else: args = p.parse_args() - args = args.__dict__ + nvar = [v for v in list(args.__dict__.values()) + [[]] if isinstance(v, list)] + args = {k:v for k,v in args.__dict__.items() if not isinstance(v, list)} xtra = otherwise(args.pop('xtra', ''), eq(1), p.prog) tfunc = trace(func) if args.pop('pdb', False) else func - return tfunc(**merge(args, args_from_prog(func, xtra))) + tfunc = partial(tfunc, **merge(args, args_from_prog(func, xtra))) + return tfunc(*nvar[0]) mod = inspect.getmodule(inspect.currentframe().f_back) if getattr(mod, '__name__', '') =="__main__": diff --git a/nbs/06_docments.ipynb b/nbs/06_docments.ipynb index 62542f54..0bc9eaff 100644 --- a/nbs/06_docments.ipynb +++ b/nbs/06_docments.ipynb @@ -303,9 +303,9 @@ " line -= 1\n", " return dedent('\\n'.join(reversed(res))) if res else None\n", "\n", - "def _get_full(anno, name, default, docs):\n", + "def _get_full(anno, name, default, docs, kind):\n", " if anno==empty and default!=empty: anno = type(default)\n", - " return AttrDict(docment=docs.get(name), anno=anno, default=default)" + " return AttrDict(docment=docs.get(name), anno=anno, default=default, kind=kind)" ] }, { @@ -415,8 +415,8 @@ "\n", " if isinstance(s,str): s = eval(s)\n", " sig = signature(s)\n", - " res = {arg:_get_full(p.annotation, p.name, p.default, docs) for arg,p in sig.parameters.items()}\n", - " if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs)\n", + " res = {arg:_get_full(p.annotation, p.name, p.default, docs, p.kind) for arg,p in sig.parameters.items()}\n", + " if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs, '')\n", " res = _merge_docs(res, nps)\n", " if eval_str:\n", " hints = type_hints(s)\n", @@ -438,7 +438,6 @@ " r = {}\n", " params = set(signature(elt).parameters)\n", " params.add('return')\n", - "\n", " def _update_docments(f, r):\n", " if hasattr(f, '__delwrap__'): _update_docments(f.__delwrap__, r)\n", " r.update({k:v for k,v in _docments(f, **kwargs).items() if k in params\n", @@ -456,6 +455,28 @@ "The returned `dict` has parameter names as keys, docments as values. The return value comment appears in the `return`, unless `returns=False`. Using the `add` definition above, we get:" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_items([('args', {'docment': None, 'anno': , 'default': , 'kind': <_ParameterKind.VAR_POSITIONAL: 2>})])" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def timesheet(*args):\n", + " pass\n", + "docments(timesheet, full=True, returns=False, eval_str=True).items()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -510,23 +531,31 @@ "```json\n", "{ 'a': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the 1st number to add'},\n", + " 'docment': 'the 1st number to add',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'b': { 'anno': ,\n", " 'default': 0,\n", - " 'docment': 'the 2nd number to add'},\n", + " 'docment': 'the 2nd number to add',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'return': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the result of adding `a` to `b`'}}\n", + " 'docment': 'the result of adding `a` to `b`',\n", + " 'kind': ''}}\n", "```" ], "text/plain": [ "{'a': {'docment': 'the 1st number to add',\n", " 'anno': 'int',\n", - " 'default': inspect._empty},\n", - " 'b': {'docment': 'the 2nd number to add', 'anno': int, 'default': 0},\n", + " 'default': inspect._empty,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", + " 'b': {'docment': 'the 2nd number to add',\n", + " 'anno': int,\n", + " 'default': 0,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'return': {'docment': 'the result of adding `a` to `b`',\n", " 'anno': 'int',\n", - " 'default': inspect._empty}}" + " 'default': inspect._empty,\n", + " 'kind': ''}}" ] }, "execution_count": null, @@ -556,11 +585,15 @@ "```json\n", "{ 'anno': ,\n", " 'default': ,\n", - " 'docment': 'the 1st number to add'}\n", + " 'docment': 'the 1st number to add',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>}\n", "```" ], "text/plain": [ - "{'docment': 'the 1st number to add', 'anno': int, 'default': inspect._empty}" + "{'docment': 'the 1st number to add',\n", + " 'anno': int,\n", + " 'default': inspect._empty,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>}" ] }, "execution_count": null, @@ -806,23 +839,31 @@ "```json\n", "{ 'a': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the first number to add'},\n", + " 'docment': 'the first number to add',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'b': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the 2nd number to add (default: 0)'},\n", + " 'docment': 'the 2nd number to add (default: 0)',\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'return': { 'anno': 'int',\n", " 'default': ,\n", - " 'docment': 'the result'}}\n", + " 'docment': 'the result',\n", + " 'kind': ''}}\n", "```" ], "text/plain": [ "{'a': {'docment': 'the first number to add',\n", " 'anno': 'int',\n", - " 'default': inspect._empty},\n", + " 'default': inspect._empty,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", " 'b': {'docment': 'the 2nd number to add (default: 0)',\n", " 'anno': 'int',\n", - " 'default': inspect._empty},\n", - " 'return': {'docment': 'the result', 'anno': 'int', 'default': inspect._empty}}" + " 'default': inspect._empty,\n", + " 'kind': <_ParameterKind.POSITIONAL_OR_KEYWORD: 1>},\n", + " 'return': {'docment': 'the result',\n", + " 'anno': 'int',\n", + " 'default': inspect._empty,\n", + " 'kind': ''}}" ] }, "execution_count": null, diff --git a/nbs/08_script.ipynb b/nbs/08_script.ipynb index 2d3b26d9..6cf94dd0 100644 --- a/nbs/08_script.ipynb +++ b/nbs/08_script.ipynb @@ -151,7 +151,8 @@ "from functools import wraps,partial\n", "from fastcore.imports import *\n", "from fastcore.utils import *\n", - "from fastcore.docments import docments" + "from fastcore.docments import docments\n", + "from inspect import Parameter" ] }, { @@ -259,7 +260,6 @@ " else: self.default = d\n", " if self.default is not None:\n", " self.help += f\" (default: {self.default})\"\n", - "\n", " @property\n", " def pre(self): return '--' if self.opt else ''\n", " @property\n", @@ -376,7 +376,8 @@ " param = v.anno\n", " if not isinstance(param,Param): param = Param(v.docment, v.anno)\n", " param.set_default(v.default)\n", - " p.add_argument(f\"{param.pre}{k}\", **param.kwargs)\n", + " if getattr(v, 'kind', '') == Parameter.VAR_POSITIONAL: p.add_argument(f\"{param.pre}{k}\",**param.kwargs, nargs='*')\n", + " else: p.add_argument(f\"{param.pre}{k}\", **param.kwargs)\n", " p.add_argument(f\"--pdb\", help=argparse.SUPPRESS, action='store_true')\n", " p.add_argument(f\"--xtra\", help=argparse.SUPPRESS, type=str)\n", " return p" @@ -433,6 +434,39 @@ "It also works with type annotations and docments:" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: progname [-h] [args ...]\n", + "\n", + "positional arguments:\n", + " args\n", + "\n", + "optional arguments:\n", + " -h, --help show this help message and exit\n" + ] + } + ], + "source": [ + "def timesheet(*args):\n", + " pass\n", + "p = anno_parser(timesheet, 'progname')\n", + "p.print_help()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -544,10 +578,12 @@ " p = anno_parser(func)\n", " if nested: args, sys.argv[1:] = p.parse_known_args()\n", " else: args = p.parse_args()\n", - " args = args.__dict__\n", + " nvar = [v for v in list(args.__dict__.values()) + [[]] if isinstance(v, list)]\n", + " args = {k:v for k,v in args.__dict__.items() if not isinstance(v, list)}\n", " xtra = otherwise(args.pop('xtra', ''), eq(1), p.prog)\n", " tfunc = trace(func) if args.pop('pdb', False) else func\n", - " return tfunc(**merge(args, args_from_prog(func, xtra)))\n", + " tfunc = partial(tfunc, **merge(args, args_from_prog(func, xtra)))\n", + " return tfunc(*nvar[0])\n", "\n", " mod = inspect.getmodule(inspect.currentframe().f_back)\n", " if getattr(mod, '__name__', '') ==\"__main__\":\n", @@ -588,6 +624,17 @@ "test_eq(test_add(1,2), 3)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@call_parse\n", + "def timesheet(*args):\n", + " pass" + ] + }, { "cell_type": "markdown", "metadata": {}, From 8b49502767b0e37574a9949d848d177bd1e27406 Mon Sep 17 00:00:00 2001 From: deven367 Date: Thu, 30 May 2024 10:36:55 -0400 Subject: [PATCH 002/182] clean --- nbs/06_docments.ipynb | 2 +- nbs/08_script.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nbs/06_docments.ipynb b/nbs/06_docments.ipynb index 0bc9eaff..9ebbe00f 100644 --- a/nbs/06_docments.ipynb +++ b/nbs/06_docments.ipynb @@ -1001,7 +1001,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/08_script.ipynb b/nbs/08_script.ipynb index 6cf94dd0..68c8dc10 100644 --- a/nbs/08_script.ipynb +++ b/nbs/08_script.ipynb @@ -691,7 +691,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } From 86dea6c00b0eab3ca224f510a4fa2bfe7dd4c0f9 Mon Sep 17 00:00:00 2001 From: deven367 Date: Thu, 30 May 2024 10:37:32 -0400 Subject: [PATCH 003/182] clean and sync --- README.md | 3 +-- fastcore/_modidx.py | 2 +- nbs/000_tour.ipynb | 2 +- nbs/00_test.ipynb | 8 ++++---- nbs/01_basics.ipynb | 2 +- nbs/02_foundation.ipynb | 2 +- nbs/03_xtras.ipynb | 2 +- nbs/03a_parallel.ipynb | 2 +- nbs/03b_net.ipynb | 2 +- nbs/04_dispatch.ipynb | 2 +- nbs/05_transform.ipynb | 4 ++-- nbs/07_meta.ipynb | 2 +- nbs/09_xdg.ipynb | 2 +- nbs/10_style.ipynb | 2 +- nbs/_parallel_win.ipynb | 2 +- nbs/index.ipynb | 2 +- 16 files changed, 20 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 34527246..22f54624 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Welcome to fastcore -================ +# Welcome to fastcore diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 556cd19f..1498fdc5 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -594,4 +594,4 @@ 'fastcore.xtras.truncstr': ('xtras.html#truncstr', 'fastcore/xtras.py'), 'fastcore.xtras.untar_dir': ('xtras.html#untar_dir', 'fastcore/xtras.py'), 'fastcore.xtras.utc2local': ('xtras.html#utc2local', 'fastcore/xtras.py'), - 'fastcore.xtras.walk': ('xtras.html#walk', 'fastcore/xtras.py')}}} \ No newline at end of file + 'fastcore.xtras.walk': ('xtras.html#walk', 'fastcore/xtras.py')}}} diff --git a/nbs/000_tour.ipynb b/nbs/000_tour.ipynb index 84414dbc..15641a70 100644 --- a/nbs/000_tour.ipynb +++ b/nbs/000_tour.ipynb @@ -692,7 +692,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/00_test.ipynb b/nbs/00_test.ipynb index a3bffb4e..196ce40f 100644 --- a/nbs/00_test.ipynb +++ b/nbs/00_test.ipynb @@ -582,7 +582,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "" ] @@ -613,7 +613,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "" ] @@ -646,7 +646,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -798,7 +798,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index b931cd76..e6c4e25e 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -6110,7 +6110,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index ea8eff5f..c6380e63 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -2460,7 +2460,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 545c444d..b6890245 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -2394,7 +2394,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/03a_parallel.ipynb b/nbs/03a_parallel.ipynb index 6638fa0c..6cb51a47 100644 --- a/nbs/03a_parallel.ipynb +++ b/nbs/03a_parallel.ipynb @@ -598,7 +598,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/03b_net.ipynb b/nbs/03b_net.ipynb index 8696723a..9b513c3e 100644 --- a/nbs/03b_net.ipynb +++ b/nbs/03b_net.ipynb @@ -729,7 +729,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/04_dispatch.ipynb b/nbs/04_dispatch.ipynb index 3eb77291..9783d432 100644 --- a/nbs/04_dispatch.ipynb +++ b/nbs/04_dispatch.ipynb @@ -1424,7 +1424,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/05_transform.ipynb b/nbs/05_transform.ipynb index b25a384d..8967ed36 100644 --- a/nbs/05_transform.ipynb +++ b/nbs/05_transform.ipynb @@ -1781,7 +1781,7 @@ "outputs": [ { "data": { - "image/png": "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\n", + "image/png": "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", "text/plain": [ "
" ] @@ -1993,7 +1993,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/07_meta.ipynb b/nbs/07_meta.ipynb index 1f28059e..74996f1e 100644 --- a/nbs/07_meta.ipynb +++ b/nbs/07_meta.ipynb @@ -1477,7 +1477,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/09_xdg.ipynb b/nbs/09_xdg.ipynb index 7aa69465..289e90c9 100644 --- a/nbs/09_xdg.ipynb +++ b/nbs/09_xdg.ipynb @@ -343,7 +343,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/10_style.ipynb b/nbs/10_style.ipynb index 5de6fe20..a2f7a56e 100644 --- a/nbs/10_style.ipynb +++ b/nbs/10_style.ipynb @@ -470,7 +470,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/_parallel_win.ipynb b/nbs/_parallel_win.ipynb index e18ce38f..96c5aa73 100644 --- a/nbs/_parallel_win.ipynb +++ b/nbs/_parallel_win.ipynb @@ -28,7 +28,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "python3", "language": "python", "name": "python3" } diff --git a/nbs/index.ipynb b/nbs/index.ipynb index 15f66912..e223bbad 100644 --- a/nbs/index.ipynb +++ b/nbs/index.ipynb @@ -86,7 +86,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } From 78b1486bc9760a47e7df2c185f4f6f3a17a01f53 Mon Sep 17 00:00:00 2001 From: "Joshua L. Adelman" Date: Fri, 22 Nov 2024 09:56:15 -0500 Subject: [PATCH 004/182] bump min python version to 3.9 --- settings.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.ini b/settings.ini index e7102622..3b7375c6 100644 --- a/settings.ini +++ b/settings.ini @@ -9,7 +9,7 @@ author_email = infos@fast.ai copyright = fast.ai branch = master version = 1.7.21 -min_python = 3.8 +min_python = 3.9 audience = Developers language = English custom_sidebar = False From 8a6edeeac63a86b3f0f412c2800c20217fc4f6cb Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 30 Nov 2024 16:00:28 +1000 Subject: [PATCH 005/182] fixes #651 --- fastcore/_modidx.py | 1 + fastcore/basics.py | 10 +++++++++- fastcore/foundation.py | 2 +- nbs/01_basics.ipynb | 40 +++++++++++++++++++++++++++++++++------- nbs/02_foundation.ipynb | 30 +++++++++--------------------- 5 files changed, 53 insertions(+), 30 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 3dc01417..b6f2a0ec 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -155,6 +155,7 @@ 'fastcore.basics.in_': ('basics.html#in_', 'fastcore/basics.py'), 'fastcore.basics.instantiate': ('basics.html#instantiate', 'fastcore/basics.py'), 'fastcore.basics.is_array': ('basics.html#is_array', 'fastcore/basics.py'), + 'fastcore.basics.last': ('basics.html#last', 'fastcore/basics.py'), 'fastcore.basics.last_index': ('basics.html#last_index', 'fastcore/basics.py'), 'fastcore.basics.last_match': ('basics.html#last_match', 'fastcore/basics.py'), 'fastcore.basics.listify': ('basics.html#listify', 'fastcore/basics.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index 16d36336..e35aad6e 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -13,7 +13,7 @@ 'hasattrs', 'setattrs', 'try_attrs', 'GetAttrBase', 'GetAttr', 'delegate_attr', 'ShowPrint', 'Int', 'Str', 'Float', 'partition', 'flatten', 'concat', 'strcat', 'detuplify', 'replicate', 'setify', 'merge', 'range_of', 'groupby', 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle', 'zip_cycle', 'sorted_ex', - 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first', 'only', 'nested_attr', 'nested_setdefault', + 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first', 'last', 'only', 'nested_attr', 'nested_setdefault', 'nested_callable', 'nested_idx', 'set_nested_idx', 'val2idx', 'uniqueify', 'loop_first_last', 'loop_first', 'loop_last', 'first_match', 'last_match', 'fastuple', 'bind', 'mapt', 'map_ex', 'compose', 'maps', 'partialler', 'instantiate', 'using_attr', 'copy_func', 'patch_to', 'patch', 'patch_property', 'compile_re', @@ -708,6 +708,14 @@ def first(x, f=None, negate=False, **kwargs): if f: x = filter_ex(x, f=f, negate=negate, gen=True, **kwargs) return next(x, None) +# %% ../nbs/01_basics.ipynb +def last(x, f=None, negate=False, **kwargs): + "Last element of `x`, optionally filtered by `f`, or None if missing" + if f: x = filter_ex(x, f=f, negate=negate, gen=True, **kwargs) + res = None + for res in x: pass + return res + # %% ../nbs/01_basics.ipynb def only(o): "Return the only item of `o`, raise if `o` doesn't have exactly one item" diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 62ab99dc..e4324115 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -45,7 +45,7 @@ def docs(cls): return cls # %% ../nbs/02_foundation.ipynb -def coll_repr(c, max_n=10): +def coll_repr(c, max_n=20): "String repr of up to `max_n` items of (possibly lazy) collection `c`" return f'(#{len(c)}) [' + ','.join(itertools.islice(map(repr,c), max_n)) + ( '...' if len(c)>max_n else '') + ']' diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 87be81a5..4fd0854d 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -676,7 +676,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L105){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### get_class\n", "\n", @@ -688,7 +688,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L105){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### get_class\n", "\n", @@ -715,7 +715,7 @@ { "data": { "text/plain": [ - "'__main__._t(a=1, b=3)'" + "'_t(a=1, b=3)'" ] }, "execution_count": null, @@ -866,7 +866,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L149){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ignore_exceptions\n", "\n", @@ -877,7 +877,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L149){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ignore_exceptions\n", "\n", @@ -2901,7 +2901,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L516){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### GetAttr\n", "\n", @@ -2912,7 +2912,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L516){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### GetAttr\n", "\n", @@ -3966,6 +3966,32 @@ "test_eq(first([False], noop), None)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def last(x, f=None, negate=False, **kwargs):\n", + " \"Last element of `x`, optionally filtered by `f`, or None if missing\"\n", + " if f: x = filter_ex(x, f=f, negate=negate, gen=True, **kwargs)\n", + " res = None\n", + " for res in x: pass\n", + " return res" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(last(['a', 'b', 'c', 'd', 'e']), 'e')\n", + "test_eq(last([False]), False)\n", + "test_eq(last([False], noop), None)" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 2a6baa0c..1452325f 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -304,7 +304,7 @@ "outputs": [], "source": [ "#|export\n", - "def coll_repr(c, max_n=10):\n", + "def coll_repr(c, max_n=20):\n", " \"String repr of up to `max_n` items of (possibly lazy) collection `c`\"\n", " return f'(#{len(c)}) [' + ','.join(itertools.islice(map(repr,c), max_n)) + (\n", " '...' if len(c)>max_n else '') + ']'" @@ -325,26 +325,10 @@ "metadata": {}, "outputs": [], "source": [ - "test_eq(coll_repr(range(1000)), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')\n", + "test_eq(coll_repr(range(1000),10), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')\n", "test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]')\n", - "test_eq(coll_repr(range(10), 5), '(#10) [0,1,2,3,4...]')\n", - "test_eq(coll_repr(range(5), 5), '(#5) [0,1,2,3,4]')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can set the option `max_n` to optionally preview a specified number of items instead of the default:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(coll_repr(range(1000), max_n=5), '(#1000) [0,1,2,3,4...]')" + "test_eq(coll_repr(range(10), 5), '(#10) [0,1,2,3,4...]')\n", + "test_eq(coll_repr(range(5), 5), '(#5) [0,1,2,3,4]')" ] }, { @@ -759,7 +743,7 @@ { "data": { "text/plain": [ - "(#12) [11,10,9,'j',7,'k',5,4,3,2...]" + "(#12) [11,10,9,'j',7,'k',5,4,3,2,1,0]" ] }, "execution_count": null, @@ -2486,6 +2470,8 @@ "text/markdown": [ "---\n", "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L291){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### Config.find\n", "\n", "> Config.find (cfg_name, cfg_path=None, **kwargs)\n", @@ -2495,6 +2481,8 @@ "text/plain": [ "---\n", "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L291){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### Config.find\n", "\n", "> Config.find (cfg_name, cfg_path=None, **kwargs)\n", From ec4be543e91cfbe01d80cba7418fefbc114c09fa Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 30 Nov 2024 16:00:45 +1000 Subject: [PATCH 006/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69965162..4de3c964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.7.21 + +### New Features + +- Add `last` ([#651](https://github.com/AnswerDotAI/fastcore/issues/651)) + + ## 1.7.20 ### New Features From 981f2640f43668acbe898da06c328a11e3482e58 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 30 Nov 2024 16:01:50 +1000 Subject: [PATCH 007/182] settings --- settings.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.ini b/settings.ini index e7102622..f9c922fc 100644 --- a/settings.ini +++ b/settings.ini @@ -1,7 +1,7 @@ [DEFAULT] lib_name = fastcore repo = fastcore -user = fastai +user = AnswerDotAI description = Python supercharged for fastai development keywords = python author = Jeremy Howard and Sylvain Gugger @@ -18,7 +18,7 @@ status = 5 nbs_path = nbs doc_path = _docs dev_requirements = numpy nbdev>=0.2.39 matplotlib pillow torch pandas nbclassic pysymbol_llm llms-txt -git_url = https://github.com/fastai/fastcore/ +git_url = https://github.com/AnswerDotAI/fastcore/ lib_path = fastcore title = fastcore doc_host = https://fastcore.fast.ai From 8c6289220c103b8c0f4f20dd1a6ad2222e5946df Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 30 Nov 2024 16:39:07 +1000 Subject: [PATCH 008/182] release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de3c964..236fc227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ + + ## 1.7.21 ### New Features From c7814b7c4127ab2e9a6450e69942990f25528c17 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 30 Nov 2024 16:45:03 +1000 Subject: [PATCH 009/182] deps --- .github/workflows/main.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aaaf3782..e0ba7125 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos]#,windows] - py: ["3.9", "3.10", "3.11"] + py: ["3.9", "3.10", "3.11", "3.12"] include: #- os: windows # shell: "/usr/bin/bash" diff --git a/setup.py b/setup.py index f7449f29..6d8e5e90 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ } statuses = [ '1 - Planning', '2 - Pre-Alpha', '3 - Alpha', '4 - Beta', '5 - Production/Stable', '6 - Mature', '7 - Inactive' ] -py_versions = '2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10'.split() +py_versions = '3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 3.10 3.11 3.12 3.13'.split() min_python = cfg['min_python'] lic = licenses[cfg['license']] From e4a3ebfbc90aeadebdcc76f1cb7cad94410a1b16 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 30 Nov 2024 16:45:33 +1000 Subject: [PATCH 010/182] release --- CHANGELOG.md | 4 +--- fastcore/__init__.py | 2 +- fastcore/_modidx.py | 2 +- settings.ini | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 236fc227..ca1f5136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,7 @@ - - -## 1.7.21 +## 1.7.22 ### New Features diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 05230533..8f4ce78d 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.21" +__version__ = "1.7.22" diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index b6f2a0ec..22b6f104 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -3,7 +3,7 @@ d = { 'settings': { 'branch': 'master', 'doc_baseurl': '/', 'doc_host': 'https://fastcore.fast.ai', - 'git_url': 'https://github.com/fastai/fastcore/', + 'git_url': 'https://github.com/AnswerDotAI/fastcore/', 'lib_path': 'fastcore'}, 'syms': { 'fastcore.all': {}, 'fastcore.ansi': {}, diff --git a/settings.ini b/settings.ini index 253e22c4..28cadef6 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.21 +version = 1.7.22 min_python = 3.9 audience = Developers language = English From 852867e5b727fa42d9d03d199563a9fe994c701f Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 30 Nov 2024 16:47:17 +1000 Subject: [PATCH 011/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 8f4ce78d..556a30c3 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.22" +__version__ = "1.7.23" diff --git a/settings.ini b/settings.ini index 28cadef6..63387cc9 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.22 +version = 1.7.23 min_python = 3.9 audience = Developers language = English From 960cc2b994f2a460e02108f79e8ea0194f7abfa4 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 30 Nov 2024 17:06:46 +1000 Subject: [PATCH 012/182] fix ci --- .github/workflows/docs.yml | 3 +++ .github/workflows/main.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7c34451c..7f7fa66d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,9 @@ on: - master workflow_dispatch: +env: + OBJC_DISABLE_INITIALIZE_FORK_SAFETY: YES + jobs: deploy: name: Deploy to GitHub Pages diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0ba7125..2e96057b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,6 +9,9 @@ defaults: run: shell: bash +env: + OBJC_DISABLE_INITIALIZE_FORK_SAFETY: YES + jobs: fastcore: strategy: From 4560a8b2e8fddc24132735331666c8386d137f5f Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 6 Dec 2024 08:06:32 +1000 Subject: [PATCH 013/182] fixes #652 --- fastcore/_modidx.py | 2 + fastcore/basics.py | 11 +++- fastcore/foundation.py | 12 ++-- nbs/01_basics.ipynb | 132 +++++++++++++++++++++++++++++++++++++--- nbs/02_foundation.ipynb | 12 ++-- nbs/nbdev.yml | 2 +- 6 files changed, 150 insertions(+), 21 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 22b6f104..04a9eb32 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -346,6 +346,7 @@ 'fastcore.foundation.L.cycle': ('foundation.html#l.cycle', 'fastcore/foundation.py'), 'fastcore.foundation.L.enumerate': ('foundation.html#l.enumerate', 'fastcore/foundation.py'), 'fastcore.foundation.L.filter': ('foundation.html#l.filter', 'fastcore/foundation.py'), + 'fastcore.foundation.L.groupby': ('foundation.html#l.groupby', 'fastcore/foundation.py'), 'fastcore.foundation.L.itemgot': ('foundation.html#l.itemgot', 'fastcore/foundation.py'), 'fastcore.foundation.L.map': ('foundation.html#l.map', 'fastcore/foundation.py'), 'fastcore.foundation.L.map_dict': ('foundation.html#l.map_dict', 'fastcore/foundation.py'), @@ -360,6 +361,7 @@ 'fastcore.foundation.L.shuffle': ('foundation.html#l.shuffle', 'fastcore/foundation.py'), 'fastcore.foundation.L.sorted': ('foundation.html#l.sorted', 'fastcore/foundation.py'), 'fastcore.foundation.L.split': ('foundation.html#l.split', 'fastcore/foundation.py'), + 'fastcore.foundation.L.splitlines': ('foundation.html#l.splitlines', 'fastcore/foundation.py'), 'fastcore.foundation.L.starmap': ('foundation.html#l.starmap', 'fastcore/foundation.py'), 'fastcore.foundation.L.sum': ('foundation.html#l.sum', 'fastcore/foundation.py'), 'fastcore.foundation.L.unique': ('foundation.html#l.unique', 'fastcore/foundation.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index e35aad6e..4aa90633 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -25,6 +25,7 @@ # %% ../nbs/01_basics.ipynb from .imports import * import ast,builtins,pprint,types,typing +from functools import cmp_to_key from copy import copy from datetime import date try: from types import UnionType @@ -660,9 +661,13 @@ def zip_cycle(x, *args): return zip(x, *map(cycle,args)) # %% ../nbs/01_basics.ipynb -def sorted_ex(iterable, key=None, reverse=False): - "Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`" - if isinstance(key,str): k=lambda o:getattr(o,key,0) +def sorted_ex(iterable, key=None, reverse=False, cmp=None, **kwargs): + "Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`; use `cmp` comparator function or `key` with `kwargs`" + if callable(key) and kwargs: k=partial(key, **kwargs) + elif callable(cmp): + if kwargs: cmp=partial(cmp, **kwargs) + k = cmp_to_key(cmp) + elif isinstance(key,str): k=lambda o:getattr(o,key,0) elif isinstance(key,int): k=itemgetter(key) else: k=key return sorted(iterable, key=k, reverse=reverse) diff --git a/fastcore/foundation.py b/fastcore/foundation.py index e4324115..69f72ce4 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -136,7 +136,7 @@ def __eq__(self,b): if isinstance(b, (str,dict)) or callable(b): return False return all_equal(b,self) - def sorted(self, key=None, reverse=False): return self._new(sorted_ex(self, key=key, reverse=reverse)) + def sorted(self, key=None, reverse=False, cmp=None, **kwargs): return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs)) def __iter__(self): return iter(self.items.itertuples() if hasattr(self.items,'iloc') else self.items) def __contains__(self,b): return b in self.items def __reversed__(self): return self._new(reversed(self.items)) @@ -154,6 +154,8 @@ def __addi__(a,b): @classmethod def split(cls, s, sep=None, maxsplit=-1): return cls(s.split(sep,maxsplit)) @classmethod + def splitlines(cls, s, keepends=False): return cls(s.splitslines(keepends)) + @classmethod def range(cls, a, b=None, step=None): return cls(range_of(a, b=b, step=step)) def map(self, f, *args, **kwargs): return self._new(map_ex(self, f, *args, gen=False, **kwargs)) @@ -169,6 +171,7 @@ def renumerate(self): return L(renumerate(self)) def unique(self, sort=False, bidir=False, start=None): return L(uniqueify(self, sort=sort, bidir=bidir, start=start)) def val2idx(self): return val2idx(self) def cycle(self): return cycle(self) + def groupby(self, key, val=noop): return L(groupby(self, key, val=val)) def map_dict(self, f=noop, *args, **kwargs): return {k:f(k, *args,**kwargs) for k in self} def map_first(self, f=noop, g=noop, *args, **kwargs): return first(self.map(f, *args, **kwargs), g) @@ -201,8 +204,9 @@ def setattrs(self, attr, val): [setattr(o,attr,val) for o in self] __getitem__="Retrieve `idx` (can be list of indices, or mask, or int) items", range="Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`", split="Class Method: Same as `str.split`, but returns an `L`", + splitlines="Class Method: Same as `str.splitlines`, but returns an `L`", copy="Same as `list.copy`, but returns an `L`", - sorted="New `L` sorted by `key`. If key is str use `attrgetter`; if int use `itemgetter`", + sorted="New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`", unique="Unique items, in stable order", val2idx="Dict from value to index", filter="Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`", @@ -217,6 +221,7 @@ def setattrs(self, attr, val): [setattr(o,attr,val) for o in self] cycle="Same as `itertools.cycle`", enumerate="Same as `enumerate`", renumerate="Same as `renumerate`", + groupby="Same as `groupby`", zip="Create new `L` with `zip(*items)`", zipwith="Create new `L` with `self` zip with each of `*rest`", map_zip="Combine `zip` and `starmap`", @@ -226,8 +231,7 @@ def setattrs(self, attr, val): [setattr(o,attr,val) for o in self] reduce="Wrapper for `functools.reduce`", sum="Sum of the items", product="Product of the items", - setattrs="Call `setattr` on all items" - ) + setattrs="Call `setattr` on all items") # %% ../nbs/02_foundation.ipynb # Here we are fixing the signature of L. What happens is that the __call__ method on the MetaClass of L shadows the __init__ diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 4fd0854d..7eeb0a66 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -18,6 +18,7 @@ "#|export\n", "from fastcore.imports import *\n", "import ast,builtins,pprint,types,typing\n", + "from functools import cmp_to_key\n", "from copy import copy\n", "from datetime import date\n", "try: from types import UnionType\n", @@ -671,12 +672,20 @@ "execution_count": null, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jhoward/miniforge3/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, { "data": { "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### get_class\n", "\n", @@ -688,7 +697,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### get_class\n", "\n", @@ -866,7 +875,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ignore_exceptions\n", "\n", @@ -877,7 +886,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ignore_exceptions\n", "\n", @@ -2901,7 +2910,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### GetAttr\n", "\n", @@ -2912,7 +2921,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### GetAttr\n", "\n", @@ -3833,14 +3842,119 @@ "outputs": [], "source": [ "#|export\n", - "def sorted_ex(iterable, key=None, reverse=False):\n", - " \"Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`\"\n", - " if isinstance(key,str): k=lambda o:getattr(o,key,0)\n", + "def sorted_ex(iterable, key=None, reverse=False, cmp=None, **kwargs):\n", + " \"Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`; use `cmp` comparator function or `key` with `kwargs`\"\n", + " if callable(key) and kwargs: k=partial(key, **kwargs)\n", + " elif callable(cmp):\n", + " if kwargs: cmp=partial(cmp, **kwargs)\n", + " k = cmp_to_key(cmp)\n", + " elif isinstance(key,str): k=lambda o:getattr(o,key,0)\n", " elif isinstance(key,int): k=itemgetter(key)\n", " else: k=key\n", " return sorted(iterable, key=k, reverse=reverse)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Attributes can be used for sorting by passing their name as a string:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class TestObj:\n", + " def __init__(self, x): self.x = x\n", + "objs = [TestObj(i) for i in [3,1,2]]\n", + "test_eq([o.x for o in sorted_ex(objs, 'x')], [1,2,3])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Tuple/list items can be sorted by index position:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "items = [(1,'c'), (2,'b'), (3,'a')]\n", + "test_eq(sorted_ex(items, 1), [(3,'a'), (2,'b'), (1,'c')])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A custom key function transforms values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(sorted_ex([3,1,2], lambda x: -x), [3,2,1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can use a comparison function (returning -1/1/0):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(sorted_ex([3,1,2], cmp=lambda a,b: 1 if a>b else -1 if a Date: Fri, 6 Dec 2024 08:07:15 +1000 Subject: [PATCH 014/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca1f5136..dfc821ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.7.23 + +### New Features + +- Add `cmp` and kwargs to `sort_ex` ([#652](https://github.com/AnswerDotAI/fastcore/issues/652)) +- Add `groupby` to `L` + + ## 1.7.22 ### New Features From c4fe0214e1cd1580a4b72b87a1c47ab19a98beb8 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 6 Dec 2024 08:10:48 +1000 Subject: [PATCH 015/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 556a30c3..dcb52dc3 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.23" +__version__ = "1.7.24" diff --git a/settings.ini b/settings.ini index 63387cc9..2e836b16 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.23 +version = 1.7.24 min_python = 3.9 audience = Developers language = English From 574a171b58d02702c7127c1c218dcd09bf0e095e Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 8 Dec 2024 04:37:38 +1000 Subject: [PATCH 016/182] fixes #653 --- fastcore/foundation.py | 4 +- nbs/02_foundation.ipynb | 108 ++++++++++++++++++++-------------------- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 69f72ce4..9428d0d5 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -111,7 +111,9 @@ def __init__(self, items=None, *rest, use_list=False, match=None): @property def _xtra(self): return None def _new(self, items, *args, **kwargs): return type(self)(items, *args, use_list=None, **kwargs) - def __getitem__(self, idx): return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None) + def __getitem__(self, idx): + if isinstance(idx,int): return self.items[idx] + return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None) def copy(self): return self._new(self.items.copy()) def _get(self, i): diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index c46af49e..d88a1a5f 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -550,7 +550,9 @@ " @property\n", " def _xtra(self): return None\n", " def _new(self, items, *args, **kwargs): return type(self)(items, *args, use_list=None, **kwargs)\n", - " def __getitem__(self, idx): return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None)\n", + " def __getitem__(self, idx):\n", + " if isinstance(idx,int): return self.items[idx]\n", + " return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None)\n", " def copy(self): return self._new(self.items.copy())\n", "\n", " def _get(self, i):\n", @@ -1019,7 +1021,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.__getitem__\n", "\n", @@ -1030,7 +1032,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.__getitem__\n", "\n", @@ -1072,7 +1074,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L124){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L124){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.__setitem__\n", "\n", @@ -1083,7 +1085,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L124){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L124){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.__setitem__\n", "\n", @@ -1123,7 +1125,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L169){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L171){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.unique\n", "\n", @@ -1134,7 +1136,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L169){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L171){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.unique\n", "\n", @@ -1171,7 +1173,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L170){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L172){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.val2idx\n", "\n", @@ -1182,7 +1184,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L170){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L172){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.val2idx\n", "\n", @@ -1219,7 +1221,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L164){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L166){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.filter\n", "\n", @@ -1230,7 +1232,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L164){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L166){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.filter\n", "\n", @@ -1288,7 +1290,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L160){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L162){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.argwhere\n", "\n", @@ -1299,7 +1301,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L160){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L162){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.argwhere\n", "\n", @@ -1336,7 +1338,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L163){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.argfirst\n", "\n", @@ -1347,7 +1349,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L163){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.argfirst\n", "\n", @@ -1385,7 +1387,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L159){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map\n", "\n", @@ -1396,7 +1398,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L159){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map\n", "\n", @@ -1482,7 +1484,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L172){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L175){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_dict\n", "\n", @@ -1493,7 +1495,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L172){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L175){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_dict\n", "\n", @@ -1531,7 +1533,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L184){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L187){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.zip\n", "\n", @@ -1542,7 +1544,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L184){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L187){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.zip\n", "\n", @@ -1591,7 +1593,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L186){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L189){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_zip\n", "\n", @@ -1602,7 +1604,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L186){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L189){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_zip\n", "\n", @@ -1640,7 +1642,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L185){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L188){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.zipwith\n", "\n", @@ -1651,7 +1653,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L185){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L188){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.zipwith\n", "\n", @@ -1690,7 +1692,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L187){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L190){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_zipwith\n", "\n", @@ -1701,7 +1703,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L187){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L190){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_zipwith\n", "\n", @@ -1738,7 +1740,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L179){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.itemgot\n", "\n", @@ -1749,7 +1751,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L179){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.itemgot\n", "\n", @@ -1786,7 +1788,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L180){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L183){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.attrgot\n", "\n", @@ -1797,7 +1799,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L180){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L183){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.attrgot\n", "\n", @@ -1840,24 +1842,24 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.sorted\n", "\n", - "> L.sorted (key=None, reverse=False)\n", + "> L.sorted (key=None, reverse=False, cmp=None, **kwargs)\n", "\n", - "*New `L` sorted by `key`. If key is str use `attrgetter`; if int use `itemgetter`*" + "*New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`*" ], "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.sorted\n", "\n", - "> L.sorted (key=None, reverse=False)\n", + "> L.sorted (key=None, reverse=False, cmp=None, **kwargs)\n", "\n", - "*New `L` sorted by `key`. If key is str use `attrgetter`; if int use `itemgetter`*" + "*New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`*" ] }, "execution_count": null, @@ -1888,7 +1890,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L155){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L155){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.split\n", "\n", @@ -1899,7 +1901,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L155){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L155){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.split\n", "\n", @@ -1936,7 +1938,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L159){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.range\n", "\n", @@ -1947,7 +1949,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L159){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.range\n", "\n", @@ -1985,7 +1987,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L193){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L196){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.concat\n", "\n", @@ -1996,7 +1998,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L193){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L196){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.concat\n", "\n", @@ -2033,7 +2035,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L115){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L115){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.copy\n", "\n", @@ -2044,7 +2046,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L115){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L115){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.copy\n", "\n", @@ -2082,7 +2084,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L173){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_first\n", "\n", @@ -2093,7 +2095,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L173){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_first\n", "\n", @@ -2131,7 +2133,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L197){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L200){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.setattrs\n", "\n", @@ -2142,7 +2144,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L197){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L200){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.setattrs\n", "\n", @@ -2380,7 +2382,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L277){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L281){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.get\n", "\n", @@ -2389,7 +2391,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L277){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L281){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.get\n", "\n", @@ -2474,7 +2476,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L291){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L295){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.find\n", "\n", @@ -2485,7 +2487,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/fastai/fastcore/blob/master/fastcore/foundation.py#L291){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L295){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.find\n", "\n", From 9a5726da54d5c9dff530995be8c5ca9bb0313047 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 8 Dec 2024 04:37:56 +1000 Subject: [PATCH 017/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc821ce..81b8b693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.7.24 + +### New Features + +- Optimise case of indexing `L` with an `int` ([#653](https://github.com/AnswerDotAI/fastcore/issues/653)) + + ## 1.7.23 ### New Features From c2a322b6c11d0af4494b17d53e7139e91608e5d2 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 8 Dec 2024 04:42:03 +1000 Subject: [PATCH 018/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index dcb52dc3..e0862ab5 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.24" +__version__ = "1.7.25" diff --git a/settings.ini b/settings.ini index 2e836b16..791c50ca 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.24 +version = 1.7.25 min_python = 3.9 audience = Developers language = English From b1df2f61335ce179d11eb8dc987b23d287c4300d Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 8 Dec 2024 05:17:37 +1000 Subject: [PATCH 019/182] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e96057b..9188a00b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: pull_request: push: branches: - - master + - main defaults: run: shell: bash From 77fefc4e465330cb942595327de249ad9260e97e Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 9 Dec 2024 14:24:28 +1000 Subject: [PATCH 020/182] fixes #654 --- fastcore/_modidx.py | 1 + fastcore/basics.py | 12 +++-- fastcore/foundation.py | 2 +- nbs/01_basics.ipynb | 58 ++++++++++++++++++----- nbs/02_foundation.ipynb | 102 ++++++++++++++++++++++------------------ 5 files changed, 112 insertions(+), 63 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 04a9eb32..6e9bd13a 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -90,6 +90,7 @@ 'fastcore.basics._clsmethod': ('basics.html#_clsmethod', 'fastcore/basics.py'), 'fastcore.basics._clsmethod.__get__': ('basics.html#_clsmethod.__get__', 'fastcore/basics.py'), 'fastcore.basics._clsmethod.__init__': ('basics.html#_clsmethod.__init__', 'fastcore/basics.py'), + 'fastcore.basics._conv_key': ('basics.html#_conv_key', 'fastcore/basics.py'), 'fastcore.basics._eval_type': ('basics.html#_eval_type', 'fastcore/basics.py'), 'fastcore.basics._get_op': ('basics.html#_get_op', 'fastcore/basics.py'), 'fastcore.basics._ispy3_10': ('basics.html#_ispy3_10', 'fastcore/basics.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index 4aa90633..cca2170d 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -618,12 +618,16 @@ def range_of(x): return list(range(len(x))) # %% ../nbs/01_basics.ipynb +def _conv_key(k): + if isinstance(k,int): return itemgetter(k) + elif isinstance(k,str): return attrgetter(k) + elif isinstance(k,tuple): return lambda x: tuple(_conv_key(o)(x) for o in k) + return k + def groupby(x, key, val=noop): "Like `itertools.groupby` but doesn't need to be sorted, and isn't lazy, plus some extensions" - if isinstance(key,int): key = itemgetter(key) - elif isinstance(key,str): key = attrgetter(key) - if isinstance(val,int): val = itemgetter(val) - elif isinstance(val,str): val = attrgetter(val) + key = _conv_key(key) + val = _conv_key(val) res = {} for o in x: res.setdefault(key(o), []).append(val(o)) return res diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 9428d0d5..0920e8b9 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -112,7 +112,7 @@ def __init__(self, items=None, *rest, use_list=False, match=None): def _xtra(self): return None def _new(self, items, *args, **kwargs): return type(self)(items, *args, use_list=None, **kwargs) def __getitem__(self, idx): - if isinstance(idx,int): return self.items[idx] + if isinstance(idx,int) and not hasattr(self.items,'iloc'): return self.items[idx] return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None) def copy(self): return self._new(self.items.copy()) diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 7eeb0a66..3b0631e7 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -685,7 +685,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### get_class\n", "\n", @@ -697,7 +697,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### get_class\n", "\n", @@ -875,7 +875,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L158){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ignore_exceptions\n", "\n", @@ -886,7 +886,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L158){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ignore_exceptions\n", "\n", @@ -2910,7 +2910,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L525){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### GetAttr\n", "\n", @@ -2921,7 +2921,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L524){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L525){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### GetAttr\n", "\n", @@ -3601,12 +3601,16 @@ "outputs": [], "source": [ "#|export\n", + "def _conv_key(k):\n", + " if isinstance(k,int): return itemgetter(k)\n", + " elif isinstance(k,str): return attrgetter(k)\n", + " elif isinstance(k,tuple): return lambda x: tuple(_conv_key(o)(x) for o in k)\n", + " return k\n", + "\n", "def groupby(x, key, val=noop):\n", " \"Like `itertools.groupby` but doesn't need to be sorted, and isn't lazy, plus some extensions\"\n", - " if isinstance(key,int): key = itemgetter(key)\n", - " elif isinstance(key,str): key = attrgetter(key)\n", - " if isinstance(val,int): val = itemgetter(val)\n", - " elif isinstance(val,str): val = attrgetter(val)\n", + " key = _conv_key(key)\n", + " val = _conv_key(val)\n", " res = {}\n", " for o in x: res.setdefault(key(o), []).append(val(o))\n", " return res" @@ -3625,7 +3629,39 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here's an example of how to *invert* a grouping, using an `int` as `key` (which uses `itemgetter`; passing a `str` will use `attrgetter`), and using a `val` function:" + "You can use an `int` as `key` or `val` (which uses `itemgetter`; passing a `str` will use `attrgetter`), eg:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(groupby('aa ab bb'.split(), 0), {'a':['aa','ab'], 'b':['bb']})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "...and you can use a tuple as `key` or `val` (which creates a tuple from the provided keys or vals), eg:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(groupby('aaa abc bba'.split(), 0, (1,2)), {'a':[('a','a'),('b','c')], 'b':[('b','a')]})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's an example of how to *invert* a grouping, and using a `val` function:" ] }, { diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index d88a1a5f..6268ce71 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -255,6 +255,14 @@ "execution_count": null, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/jhoward/miniforge3/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, { "data": { "text/markdown": [ @@ -551,7 +559,7 @@ " def _xtra(self): return None\n", " def _new(self, items, *args, **kwargs): return type(self)(items, *args, use_list=None, **kwargs)\n", " def __getitem__(self, idx):\n", - " if isinstance(idx,int): return self.items[idx]\n", + " if isinstance(idx,int) and not hasattr(self.items,'iloc'): return self.items[idx]\n", " return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None)\n", " def copy(self): return self._new(self.items.copy())\n", "\n", @@ -1074,7 +1082,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L124){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L126){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.__setitem__\n", "\n", @@ -1085,7 +1093,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L124){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L126){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.__setitem__\n", "\n", @@ -1125,7 +1133,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L171){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L173){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.unique\n", "\n", @@ -1136,7 +1144,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L171){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L173){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.unique\n", "\n", @@ -1173,7 +1181,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L172){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L174){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.val2idx\n", "\n", @@ -1184,7 +1192,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L172){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L174){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.val2idx\n", "\n", @@ -1221,7 +1229,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L166){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L168){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.filter\n", "\n", @@ -1232,7 +1240,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L166){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L168){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.filter\n", "\n", @@ -1290,7 +1298,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L162){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L164){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.argwhere\n", "\n", @@ -1301,7 +1309,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L162){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L164){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.argwhere\n", "\n", @@ -1338,7 +1346,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L163){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L165){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.argfirst\n", "\n", @@ -1349,7 +1357,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L163){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L165){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.argfirst\n", "\n", @@ -1387,7 +1395,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L163){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map\n", "\n", @@ -1398,7 +1406,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L163){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map\n", "\n", @@ -1484,7 +1492,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L175){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L177){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_dict\n", "\n", @@ -1495,7 +1503,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L175){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L177){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_dict\n", "\n", @@ -1533,7 +1541,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L187){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L189){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.zip\n", "\n", @@ -1544,7 +1552,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L187){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L189){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.zip\n", "\n", @@ -1593,7 +1601,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L189){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L191){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_zip\n", "\n", @@ -1604,7 +1612,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L189){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L191){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_zip\n", "\n", @@ -1642,7 +1650,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L188){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L190){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.zipwith\n", "\n", @@ -1653,7 +1661,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L188){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L190){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.zipwith\n", "\n", @@ -1692,7 +1700,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L190){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L192){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_zipwith\n", "\n", @@ -1703,7 +1711,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L190){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L192){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_zipwith\n", "\n", @@ -1740,7 +1748,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L179){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L181){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.itemgot\n", "\n", @@ -1751,7 +1759,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L179){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L181){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.itemgot\n", "\n", @@ -1788,7 +1796,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L183){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L185){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.attrgot\n", "\n", @@ -1799,7 +1807,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L183){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L185){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.attrgot\n", "\n", @@ -1842,7 +1850,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L141){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.sorted\n", "\n", @@ -1853,7 +1861,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L141){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.sorted\n", "\n", @@ -1890,7 +1898,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L155){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.split\n", "\n", @@ -1901,7 +1909,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L155){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.split\n", "\n", @@ -1938,7 +1946,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L159){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.range\n", "\n", @@ -1949,7 +1957,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L159){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.range\n", "\n", @@ -1987,7 +1995,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L196){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L198){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.concat\n", "\n", @@ -1998,7 +2006,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L196){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L198){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.concat\n", "\n", @@ -2035,7 +2043,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L115){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L117){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.copy\n", "\n", @@ -2046,7 +2054,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L115){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L117){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.copy\n", "\n", @@ -2084,7 +2092,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L178){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_first\n", "\n", @@ -2095,7 +2103,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L178){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.map_first\n", "\n", @@ -2133,7 +2141,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L200){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L202){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.setattrs\n", "\n", @@ -2144,7 +2152,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L200){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L202){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.setattrs\n", "\n", @@ -2382,7 +2390,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L281){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L283){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.get\n", "\n", @@ -2391,7 +2399,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L281){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L283){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.get\n", "\n", @@ -2476,7 +2484,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L295){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L297){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.find\n", "\n", @@ -2487,7 +2495,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L295){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L297){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.find\n", "\n", From 755b19b54e66ea134786a573c5c5b88625ed13f9 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 9 Dec 2024 14:25:10 +1000 Subject: [PATCH 021/182] release --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b8b693..e19f3fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,11 @@ -## 1.7.24 +## 1.7.25 ### New Features -- Optimise case of indexing `L` with an `int` ([#653](https://github.com/AnswerDotAI/fastcore/issues/653)) +- Handle tuples in `groupby` ([#654](https://github.com/AnswerDotAI/fastcore/issues/654)) ## 1.7.23 From 99feccca29c41f271d07a40a12bae275e7b786eb Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 9 Dec 2024 14:26:37 +1000 Subject: [PATCH 022/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index e0862ab5..314488ad 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.25" +__version__ = "1.7.26" diff --git a/settings.ini b/settings.ini index 791c50ca..a5f639a2 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.25 +version = 1.7.26 min_python = 3.9 audience = Developers language = English From a16c0b296c58760f5b4c53a3ef99a0ff4f29b32a Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 9 Dec 2024 19:09:29 +1000 Subject: [PATCH 023/182] Update main.yml --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9188a00b..cccffa95 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,7 @@ name: CI on: workflow_dispatch: pull_request: + types: [opened, synchronize, reopened] push: branches: - main From 031169eb4749de16094e99237ad305f96fbfa35e Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 10 Dec 2024 17:10:14 +1000 Subject: [PATCH 024/182] fixes #656 --- fastcore/foundation.py | 2 +- nbs/02_foundation.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 0920e8b9..b021881e 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -156,7 +156,7 @@ def __addi__(a,b): @classmethod def split(cls, s, sep=None, maxsplit=-1): return cls(s.split(sep,maxsplit)) @classmethod - def splitlines(cls, s, keepends=False): return cls(s.splitslines(keepends)) + def splitlines(cls, s, keepends=False): return cls(s.splitlines(keepends)) @classmethod def range(cls, a, b=None, step=None): return cls(range_of(a, b=b, step=step)) diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 6268ce71..76f8a88f 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -603,7 +603,7 @@ " @classmethod\n", " def split(cls, s, sep=None, maxsplit=-1): return cls(s.split(sep,maxsplit))\n", " @classmethod\n", - " def splitlines(cls, s, keepends=False): return cls(s.splitslines(keepends))\n", + " def splitlines(cls, s, keepends=False): return cls(s.splitlines(keepends))\n", " @classmethod\n", " def range(cls, a, b=None, step=None): return cls(range_of(a, b=b, step=step))\n", "\n", From 0da59d8f9b317757d6d5b1b00e6fd3bdd616346c Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 10 Dec 2024 17:10:29 +1000 Subject: [PATCH 025/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e19f3fea..d2956def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.7.26 + + +### Bugs Squashed + +- `splitlines` mis-spelled ([#656](https://github.com/AnswerDotAI/fastcore/issues/656)) + + ## 1.7.25 ### New Features From a0c28da466649164219d8827ac1c69c251823b49 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 10 Dec 2024 17:11:53 +1000 Subject: [PATCH 026/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 314488ad..e8696c89 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.26" +__version__ = "1.7.27" diff --git a/settings.ini b/settings.ini index a5f639a2..c4157ca9 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.26 +version = 1.7.27 min_python = 3.9 audience = Developers language = English From c27c5d0db10aca00d5a1448fc26e14045a8204be Mon Sep 17 00:00:00 2001 From: Pratap Vardhan Date: Fri, 13 Dec 2024 21:12:47 +0530 Subject: [PATCH 027/182] remove unused var from parse_docstring --- fastcore/docments.py | 1 - nbs/06_docments.ipynb | 1 - 2 files changed, 2 deletions(-) diff --git a/fastcore/docments.py b/fastcore/docments.py index 2827f37b..8cc33483 100644 --- a/fastcore/docments.py +++ b/fastcore/docments.py @@ -33,7 +33,6 @@ def docstring(sym): # %% ../nbs/06_docments.ipynb def parse_docstring(sym): "Parse a numpy-style docstring in `sym`" - docs = docstring(sym) return AttrDict(**docscrape.NumpyDocString(docstring(sym))) # %% ../nbs/06_docments.ipynb diff --git a/nbs/06_docments.ipynb b/nbs/06_docments.ipynb index d76e4604..01c8f98e 100644 --- a/nbs/06_docments.ipynb +++ b/nbs/06_docments.ipynb @@ -168,7 +168,6 @@ "#|export\n", "def parse_docstring(sym):\n", " \"Parse a numpy-style docstring in `sym`\"\n", - " docs = docstring(sym)\n", " return AttrDict(**docscrape.NumpyDocString(docstring(sym)))" ] }, From 17b02dc0589faedb723b4170c634645a49d72823 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 14 Dec 2024 14:20:34 +1000 Subject: [PATCH 028/182] Update docs.yml --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7f7fa66d..cde13ab1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,7 +3,7 @@ name: Deploy to GitHub Pages on: push: branches: - - master + - main workflow_dispatch: env: From 566516c841c6639fe2a7560e6b2e1da2bbf23ec6 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 14 Dec 2024 14:31:50 +1000 Subject: [PATCH 029/182] fixes #658 --- CHANGELOG.md | 5 ++++ fastcore/foundation.py | 4 +-- nbs/02_foundation.ipynb | 61 ++++++++++++++++++++++++++++++++++------- 3 files changed, 58 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2956def..c1e3e88e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ +## 1.7.27 + + + + ## 1.7.26 diff --git a/fastcore/foundation.py b/fastcore/foundation.py index b021881e..4038b588 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -173,7 +173,7 @@ def renumerate(self): return L(renumerate(self)) def unique(self, sort=False, bidir=False, start=None): return L(uniqueify(self, sort=sort, bidir=bidir, start=start)) def val2idx(self): return val2idx(self) def cycle(self): return cycle(self) - def groupby(self, key, val=noop): return L(groupby(self, key, val=val)) + def groupby(self, key, val=noop): return groupby(self, key, val=val) def map_dict(self, f=noop, *args, **kwargs): return {k:f(k, *args,**kwargs) for k in self} def map_first(self, f=noop, g=noop, *args, **kwargs): return first(self.map(f, *args, **kwargs), g) @@ -223,7 +223,7 @@ def setattrs(self, attr, val): [setattr(o,attr,val) for o in self] cycle="Same as `itertools.cycle`", enumerate="Same as `enumerate`", renumerate="Same as `renumerate`", - groupby="Same as `groupby`", + groupby="Same as `fastcore.basics.groupby`", zip="Create new `L` with `zip(*items)`", zipwith="Create new `L` with `self` zip with each of `*rest`", map_zip="Combine `zip` and `starmap`", diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 76f8a88f..e69801c3 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -255,14 +255,6 @@ "execution_count": null, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/jhoward/miniforge3/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - }, { "data": { "text/markdown": [ @@ -620,7 +612,7 @@ " def unique(self, sort=False, bidir=False, start=None): return L(uniqueify(self, sort=sort, bidir=bidir, start=start))\n", " def val2idx(self): return val2idx(self)\n", " def cycle(self): return cycle(self)\n", - " def groupby(self, key, val=noop): return L(groupby(self, key, val=val))\n", + " def groupby(self, key, val=noop): return groupby(self, key, val=val)\n", " def map_dict(self, f=noop, *args, **kwargs): return {k:f(k, *args,**kwargs) for k in self}\n", " def map_first(self, f=noop, g=noop, *args, **kwargs):\n", " return first(self.map(f, *args, **kwargs), g)\n", @@ -677,7 +669,7 @@ " cycle=\"Same as `itertools.cycle`\",\n", " enumerate=\"Same as `enumerate`\",\n", " renumerate=\"Same as `renumerate`\",\n", - " groupby=\"Same as `groupby`\",\n", + " groupby=\"Same as `fastcore.basics.groupby`\",\n", " zip=\"Create new `L` with `zip(*items)`\",\n", " zipwith=\"Create new `L` with `self` zip with each of `*rest`\",\n", " map_zip=\"Combine `zip` and `starmap`\",\n", @@ -1219,6 +1211,55 @@ "test_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1})" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "---\n", + "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "### L.groupby\n", + "\n", + "> L.groupby (key, val=)\n", + "\n", + "*Same as `groupby`*" + ], + "text/plain": [ + "---\n", + "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "### L.groupby\n", + "\n", + "> L.groupby (key, val=)\n", + "\n", + "*Same as `groupby`*" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "show_doc(L.groupby)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "words = L.split('aaa abc bba')\n", + "test_eq(words.groupby(0, (1,2)), {'a':[('a','a'),('b','c')], 'b':[('b','a')]})" + ] + }, { "cell_type": "code", "execution_count": null, From a3be3c33e112bf81554ab4112bfbfa6a2b0d95eb Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 14 Dec 2024 14:32:21 +1000 Subject: [PATCH 030/182] release --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e3e88e..45c431e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ ## 1.7.27 +### New Features +- Add `L.groupby` ([#658](https://github.com/AnswerDotAI/fastcore/issues/658)) ## 1.7.26 @@ -27,7 +29,6 @@ ### New Features - Add `cmp` and kwargs to `sort_ex` ([#652](https://github.com/AnswerDotAI/fastcore/issues/652)) -- Add `groupby` to `L` ## 1.7.22 From 37de43ddf03cf53bc6548a6be2b9bdf5e931d866 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 14 Dec 2024 14:44:58 +1000 Subject: [PATCH 031/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index e8696c89..ea3b0759 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.27" +__version__ = "1.7.28" diff --git a/settings.ini b/settings.ini index c4157ca9..176e81f8 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.27 +version = 1.7.28 min_python = 3.9 audience = Developers language = English From 27f69749064df31e8efef82a75c74f40cc3fe7a8 Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Sun, 22 Dec 2024 13:24:09 -0600 Subject: [PATCH 032/182] Update Config to support ConfigParser keyword arguments --- fastcore/foundation.py | 4 +-- nbs/02_foundation.ipynb | 64 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 4038b588..c90a10a0 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -260,11 +260,11 @@ def read_config_file(file, **kwargs): # %% ../nbs/02_foundation.ipynb class Config: "Reading and writing `ConfigParser` ini files" - def __init__(self, cfg_path, cfg_name, create=None, save=True, extra_files=None, types=None): + def __init__(self, cfg_path, cfg_name, create=None, save=True, extra_files=None, types=None, **cfg_kwargs): self.types = types or {} cfg_path = Path(cfg_path).expanduser().absolute() self.config_path,self.config_file = cfg_path,cfg_path/cfg_name - self._cfg = ConfigParser() + self._cfg = ConfigParser(**cfg_kwargs) self.d = self._cfg['DEFAULT'] found = [Path(o) for o in self._cfg.read(L(extra_files)+[self.config_file], encoding='utf8')] if self.config_file not in found and create is not None: diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index e69801c3..b1aace67 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -255,6 +255,14 @@ "execution_count": null, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/nathan/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + }, { "data": { "text/markdown": [ @@ -1227,7 +1235,7 @@ "\n", "> L.groupby (key, val=)\n", "\n", - "*Same as `groupby`*" + "*Same as `fastcore.basics.groupby`*" ], "text/plain": [ "---\n", @@ -1238,7 +1246,7 @@ "\n", "> L.groupby (key, val=)\n", "\n", - "*Same as `groupby`*" + "*Same as `fastcore.basics.groupby`*" ] }, "execution_count": null, @@ -2301,11 +2309,11 @@ "#|export\n", "class Config:\n", " \"Reading and writing `ConfigParser` ini files\"\n", - " def __init__(self, cfg_path, cfg_name, create=None, save=True, extra_files=None, types=None):\n", + " def __init__(self, cfg_path, cfg_name, create=None, save=True, extra_files=None, types=None, **cfg_kwargs):\n", " self.types = types or {}\n", " cfg_path = Path(cfg_path).expanduser().absolute()\n", " self.config_path,self.config_file = cfg_path,cfg_path/cfg_name\n", - " self._cfg = ConfigParser()\n", + " self._cfg = ConfigParser(**cfg_kwargs)\n", " self.d = self._cfg['DEFAULT']\n", " found = [Path(o) for o in self._cfg.read(L(extra_files)+[self.config_file], encoding='utf8')]\n", " if self.config_file not in found and create is not None:\n", @@ -2421,6 +2429,54 @@ "assert not Path('../tmp.ini').exists()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also pass in `ConfigParser` `kwargs` to change the behavior of how your configuration file will be parsed. For example, by default, inline comments are not handled by `Config`. However, if you pass in the `inline_comment_prefixes` with whatever your comment symbol is, you'll overwrite this behavior. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a complete example config file with comments\n", + "cfg_str = \"\"\"\\\n", + "[DEFAULT]\n", + "user = fastai # inline comment\n", + "\n", + "# Library configuration\n", + "lib_name = fastcore\n", + "\n", + "# Paths\n", + "some_path = test \n", + "\n", + "# Feature flags\n", + "some_bool = True\n", + "\n", + "# Numeric settings\n", + "some_num = # missing value\n", + "\"\"\"\n", + "\n", + "with open('../tmp.ini', 'w') as f:\n", + " f.write(cfg_str)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Now read it back to verify\n", + "try: cfg = Config('..', 'tmp.ini', inline_comment_prefixes=('#'))\n", + "finally: os.unlink('../tmp.ini')\n", + "test_eq(cfg.user,'fastai')\n", + "test_eq(cfg.some_num,'')" + ] + }, { "cell_type": "code", "execution_count": null, From f8ac714d857b9ee55c2a76bd4dd418558c35b0c2 Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Mon, 23 Dec 2024 09:14:06 -0600 Subject: [PATCH 033/182] fix ipywidget warning --- nbs/02_foundation.ipynb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index b1aace67..bd7488cb 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -255,14 +255,6 @@ "execution_count": null, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/nathan/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - }, { "data": { "text/markdown": [ From 1972669f647d11d179c70c818e237b6eafe0097e Mon Sep 17 00:00:00 2001 From: Isaac Flath Date: Thu, 26 Dec 2024 11:46:35 -0700 Subject: [PATCH 034/182] Support args kwargs --- fastcore/docments.py | 14 +++++-- nbs/06_docments.ipynb | 93 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/fastcore/docments.py b/fastcore/docments.py index 8cc33483..02efe9ed 100644 --- a/fastcore/docments.py +++ b/fastcore/docments.py @@ -65,13 +65,19 @@ def _clean_comment(s): res = _clean_re.findall(s) return res[0] if res else None -def _param_locs(s, returns=True): +def _param_locs(s, returns=True, args_kwargs=False): "`dict` of parameter line numbers to names" body = _parses(s).body - if len(body)==1: #or not isinstance(body[0], FunctionDef): return None + if len(body)==1: defn = body[0] if isinstance(defn, (FunctionDef, AsyncFunctionDef)): res = {arg.lineno:arg.arg for arg in defn.args.args} + # Add *args if present + if defn.args.vararg and args_kwargs: res[defn.args.vararg.lineno] = defn.args.vararg.arg + # Add keyword-only args + if args_kwargs: res.update({arg.lineno:arg.arg for arg in defn.args.kwonlyargs}) + # Add **kwargs if present + if defn.args.kwarg and args_kwargs: res[defn.args.kwarg.lineno] = defn.args.kwarg.arg if returns and defn.returns: res[defn.returns.lineno] = 'return' return res elif isdataclass(s): @@ -133,12 +139,12 @@ def qual_name(obj): return get_name(obj) # %% ../nbs/06_docments.ipynb -def _docments(s, returns=True, eval_str=False): +def _docments(s, returns=True, eval_str=False, args_kwargs=False): "`dict` of parameter names to 'docment-style' comments in function or string `s`" nps = parse_docstring(s) if isclass(s) and not is_dataclass(s): s = s.__init__ # Constructor for a class comments = {o.start[0]:_clean_comment(o.string) for o in _tokens(s) if o.type==COMMENT} - parms = _param_locs(s, returns=returns) or {} + parms = _param_locs(s, returns=returns, args_kwargs=args_kwargs) or {} docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()} sig = signature_ex(s, True) diff --git a/nbs/06_docments.ipynb b/nbs/06_docments.ipynb index 01c8f98e..3d1eb751 100644 --- a/nbs/06_docments.ipynb +++ b/nbs/06_docments.ipynb @@ -237,13 +237,19 @@ " res = _clean_re.findall(s)\n", " return res[0] if res else None\n", "\n", - "def _param_locs(s, returns=True):\n", + "def _param_locs(s, returns=True, args_kwargs=False):\n", " \"`dict` of parameter line numbers to names\"\n", " body = _parses(s).body\n", - " if len(body)==1: #or not isinstance(body[0], FunctionDef): return None\n", + " if len(body)==1:\n", " defn = body[0]\n", " if isinstance(defn, (FunctionDef, AsyncFunctionDef)):\n", " res = {arg.lineno:arg.arg for arg in defn.args.args}\n", + " # Add *args if present\n", + " if defn.args.vararg and args_kwargs: res[defn.args.vararg.lineno] = defn.args.vararg.arg\n", + " # Add keyword-only args\n", + " if args_kwargs: res.update({arg.lineno:arg.arg for arg in defn.args.kwonlyargs})\n", + " # Add **kwargs if present\n", + " if defn.args.kwarg and args_kwargs: res[defn.args.kwarg.lineno] = defn.args.kwarg.arg\n", " if returns and defn.returns: res[defn.returns.lineno] = 'return'\n", " return res\n", " elif isdataclass(s):\n", @@ -380,12 +386,12 @@ "outputs": [], "source": [ "#|export\n", - "def _docments(s, returns=True, eval_str=False):\n", + "def _docments(s, returns=True, eval_str=False, args_kwargs=False):\n", " \"`dict` of parameter names to 'docment-style' comments in function or string `s`\"\n", " nps = parse_docstring(s)\n", " if isclass(s) and not is_dataclass(s): s = s.__init__ # Constructor for a class\n", " comments = {o.start[0]:_clean_comment(o.string) for o in _tokens(s) if o.type==COMMENT}\n", - " parms = _param_locs(s, returns=returns) or {}\n", + " parms = _param_locs(s, returns=returns, args_kwargs=args_kwargs) or {}\n", " docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()}\n", "\n", " sig = signature_ex(s, True)\n", @@ -466,6 +472,47 @@ "docments(add)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "```json\n", + "{ 'a': 'the 1st number to add',\n", + " 'args': 'some args',\n", + " 'b': 'the 2nd number to add',\n", + " 'kwargs': 'Passed to the `example` function',\n", + " 'return': 'the result of adding `a` to `b`'}\n", + "```" + ], + "text/plain": [ + "{'args': 'some args',\n", + " 'a': 'the 1st number to add',\n", + " 'b': 'the 2nd number to add',\n", + " 'kwargs': 'Passed to the `example` function',\n", + " 'return': 'the result of adding `a` to `b`'}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def add(*args, # some args\n", + " a:int, # the 1st number to add\n", + " b=0, # the 2nd number to add\n", + " **kwargs, # Passed to the `example` function\n", + ")->int: # the result of adding `a` to `b`\n", + " \"The sum of two numbers.\"\n", + " return a+b\n", + "\n", + "docments(add, args_kwargs=True)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -910,6 +957,37 @@ "docments(_b)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "```json\n", + "{'a': 'First', 'b': 'Second', 'return': None}\n", + "```" + ], + "text/plain": [ + "{'a': 'First', 'return': None, 'b': 'Second'}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def _a(a:int=2): return a # First\n", + "\n", + "@delegates(_a)\n", + "def _b(b:str, # Second\n", + " **kwargs): \n", + " return b, (_a(**kwargs)) \n", + "docments(_b)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1062,13 +1140,6 @@ "#|hide\n", "import nbdev; nbdev.nbdev_export()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 2ba8cfe28f0ee106b195703610ff89b97d225a66 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 27 Dec 2024 06:48:32 +1000 Subject: [PATCH 035/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c431e9..9a9c80d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.7.28 + +### New Features + +- Support documenting args and kwargs ([#660](https://github.com/AnswerDotAI/fastcore/pull/660)), thanks to [@Isaac-Flath](https://github.com/Isaac-Flath) +- Update Config to support ConfigParser keyword arguments ([#659](https://github.com/AnswerDotAI/fastcore/pull/659)), thanks to [@ncoop57](https://github.com/ncoop57) + + ## 1.7.27 ### New Features From 6db38dcd086508361fb99ea78c459b5039f7a74b Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 27 Dec 2024 06:49:17 +1000 Subject: [PATCH 036/182] CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a9c80d5..074d3a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,7 +117,7 @@ ### Bugs Squashed -- `dict2obj does not handle `list_func` correctly or pass it recursively ([#630](https://github.com/fastai/fastcore/issues/630)) +- `dict2obj` does not handle `list_func` correctly or pass it recursively ([#630](https://github.com/fastai/fastcore/issues/630)) ## 1.7.9 From a44c6f259f1d05b65b75320a40fd7e020682fd1a Mon Sep 17 00:00:00 2001 From: Isaac Flath Date: Thu, 26 Dec 2024 14:18:47 -0700 Subject: [PATCH 037/182] Change single line docments delegates example to multiline --- nbs/06_docments.ipynb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nbs/06_docments.ipynb b/nbs/06_docments.ipynb index 3d1eb751..1d1fd537 100644 --- a/nbs/06_docments.ipynb +++ b/nbs/06_docments.ipynb @@ -952,7 +952,9 @@ "def _a(a:int=2): return a # First\n", "\n", "@delegates(_a)\n", - "def _b(b:str, **kwargs): return b, (_a(**kwargs)) # Second\n", + "def _b(b:str, # Second\n", + " **kwargs): \n", + " return b, (_a(**kwargs)) \n", "\n", "docments(_b)" ] From 57d2eea27823788c7686e17a14199e855855d279 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 27 Dec 2024 07:52:21 +1000 Subject: [PATCH 038/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index ea3b0759..351f3f64 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.28" +__version__ = "1.7.29" diff --git a/settings.ini b/settings.ini index 176e81f8..dbe79182 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.28 +version = 1.7.29 min_python = 3.9 audience = Developers language = English From 08b35f8c63a775b1c0eb107a9126360c7e58fc50 Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Mon, 30 Dec 2024 12:34:42 -0600 Subject: [PATCH 039/182] Add support for version flag in Param class --- fastcore/script.py | 10 +++-- nbs/08_script.ipynb | 91 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/fastcore/script.py b/fastcore/script.py index ccec772c..23d81f34 100644 --- a/fastcore/script.py +++ b/fastcore/script.py @@ -39,16 +39,20 @@ def clean_type_str(x:str): class Param: "A parameter in a function used in `anno_parser` or `call_parse`" def __init__(self, help="", type=None, opt=True, action=None, nargs=None, const=None, - choices=None, required=None, default=None): - if type in (store_true,bool): type,action,default=None,'store_true' ,False + choices=None, required=None, default=None, version=None): + if type in (store_true,bool): type,action,default=None,'store_true',False if type==store_false: type,action,default=None,'store_false',True if type and isinstance(type,typing.Type) and issubclass(type,enum.Enum) and not choices: choices=list(type) help = help or "" store_attr() def set_default(self, d): + if self.action == "version": + if d != inspect.Parameter.empty: self.version = d + self.opt = True + return if self.default is None: - if d==inspect.Parameter.empty: self.opt = False + if d == inspect.Parameter.empty: self.opt = False else: self.default = d if self.default is not None: self.help += f" (default: {self.default})" diff --git a/nbs/08_script.ipynb b/nbs/08_script.ipynb index 94e60638..6b466caf 100644 --- a/nbs/08_script.ipynb +++ b/nbs/08_script.ipynb @@ -246,16 +246,20 @@ "class Param:\n", " \"A parameter in a function used in `anno_parser` or `call_parse`\"\n", " def __init__(self, help=\"\", type=None, opt=True, action=None, nargs=None, const=None,\n", - " choices=None, required=None, default=None):\n", - " if type in (store_true,bool): type,action,default=None,'store_true' ,False\n", + " choices=None, required=None, default=None, version=None):\n", + " if type in (store_true,bool): type,action,default=None,'store_true',False\n", " if type==store_false: type,action,default=None,'store_false',True\n", " if type and isinstance(type,typing.Type) and issubclass(type,enum.Enum) and not choices: choices=list(type)\n", " help = help or \"\"\n", " store_attr()\n", "\n", " def set_default(self, d):\n", + " if self.action == \"version\":\n", + " if d != inspect.Parameter.empty: self.version = d\n", + " self.opt = True\n", + " return\n", " if self.default is None:\n", - " if d==inspect.Parameter.empty: self.opt = False\n", + " if d == inspect.Parameter.empty: self.opt = False\n", " else: self.default = d\n", " if self.default is not None:\n", " self.help += f\" (default: {self.default})\"\n", @@ -288,7 +292,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Each parameter in your function should have an annotation `Param(...)`. You can pass the following when calling `Param`: `help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required` (i.e. it takes the same parameters as `argparse.ArgumentParser.add_argument`, plus `opt`). Except for `opt`, all of these are just passed directly to `argparse`, so you have all the power of that module at your disposal. Generally you'll want to pass at least `help` (since this is provided as the help string for that parameter) and `type` (to ensure that you get the type of data you expect).\n", + "Each parameter in your function should have an annotation `Param(...)`. You can pass the following when calling `Param`: `help`,`type`,`opt`,`action`,`nargs`,`const`,`choices`,`required`, `version` (i.e. it takes the same parameters as `argparse.ArgumentParser.add_argument`, plus `opt`). Except for `opt`, all of these are just passed directly to `argparse`, so you have all the power of that module at your disposal. Generally you'll want to pass at least `help` (since this is provided as the help string for that parameter) and `type` (to ensure that you get the type of data you expect).\n", "\n", "`opt` is a bool that defines whether a param is optional or required (positional) - but you'll generally not need to set this manually, because fastcore.script will set it for you automatically based on *default* values. You should provide a default (after the `=`) for any *optional* parameters. If you don't provide a default for a parameter, then it will be a *positional* parameter.\n", "\n", @@ -398,7 +402,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "usage: progname [-h] [--b B] [--c {aa,bb,cc}] required a\n", + "usage: progname [-h] [--v] [--b B] [--c {aa,bb,cc}] required a\n", "\n", "my docs\n", "\n", @@ -406,8 +410,9 @@ " required Required param\n", " a param 1\n", "\n", - "optional arguments:\n", + "options:\n", " -h, --help show this help message and exit\n", + " --v Print version\n", " --b B param 2 (default: test)\n", " --c {aa,bb,cc} param 3 (default: aa)\n" ] @@ -417,6 +422,7 @@ "_en = str_enum('_en', 'aa','bb','cc')\n", "def f(required:Param(\"Required param\", int),\n", " a:Param(\"param 1\", bool_arg),\n", + " v:Param(\"Print version\", action='version', version='%(prog)s 2.0.0'),\n", " b:Param(\"param 2\", str)=\"test\",\n", " c:Param(\"param 3\", _en)=_en.aa):\n", " \"my docs\"\n", @@ -426,6 +432,77 @@ "p.print_help()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also check the version and help flaggs are working (you can ignore the error message below, this is expected behavior)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "progname 2.0.0\n" + ] + }, + { + "ename": "SystemExit", + "evalue": "0", + "output_type": "error", + "traceback": [ + "An exception has occurred, use %tb to see the full traceback.\n", + "\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n" + ] + } + ], + "source": [ + "p.parse_args(['--v'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: progname [-h] [--v] [--b B] [--c {aa,bb,cc}] required a\n", + "\n", + "my docs\n", + "\n", + "positional arguments:\n", + " required Required param\n", + " a param 1\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + " --v Print version\n", + " --b B param 2 (default: test)\n", + " --c {aa,bb,cc} param 3 (default: aa)\n" + ] + }, + { + "ename": "SystemExit", + "evalue": "0", + "output_type": "error", + "traceback": [ + "An exception has occurred, use %tb to see the full traceback.\n", + "\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n" + ] + } + ], + "source": [ + "p.parse_args(['-h'])" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -450,7 +527,7 @@ " required Required param\n", " a param 1\n", "\n", - "optional arguments:\n", + "options:\n", " -h, --help show this help message and exit\n", " --b B param 2 (default: test)\n", " --c {aa,bb,cc} param 3 (default: aa)\n" From 35fad3ba18435d9c54e7b19b58e3390ea258d034 Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Mon, 30 Dec 2024 12:40:15 -0600 Subject: [PATCH 040/182] Update version and help flag test in notebook --- nbs/08_script.ipynb | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/nbs/08_script.ipynb b/nbs/08_script.ipynb index 6b466caf..905d3263 100644 --- a/nbs/08_script.ipynb +++ b/nbs/08_script.ipynb @@ -436,7 +436,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can also check the version and help flaggs are working (you can ignore the error message below, this is expected behavior)" + "We can also check the version and help flaggs are working." ] }, { @@ -450,19 +450,11 @@ "text": [ "progname 2.0.0\n" ] - }, - { - "ename": "SystemExit", - "evalue": "0", - "output_type": "error", - "traceback": [ - "An exception has occurred, use %tb to see the full traceback.\n", - "\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n" - ] } ], "source": [ - "p.parse_args(['--v'])" + "try: p.parse_args(['--v'])\n", + "except: pass" ] }, { @@ -488,19 +480,11 @@ " --b B param 2 (default: test)\n", " --c {aa,bb,cc} param 3 (default: aa)\n" ] - }, - { - "ename": "SystemExit", - "evalue": "0", - "output_type": "error", - "traceback": [ - "An exception has occurred, use %tb to see the full traceback.\n", - "\u001b[0;31mSystemExit\u001b[0m\u001b[0;31m:\u001b[0m 0\n" - ] } ], "source": [ - "p.parse_args(['-h'])" + "try: p.parse_args(['-h'])\n", + "except: pass" ] }, { From d6bd1c30ff271918ddb561e351d5bf95c2967b22 Mon Sep 17 00:00:00 2001 From: Slava Chaunin <67190162+Ssslakter@users.noreply.github.com> Date: Thu, 16 Jan 2025 00:12:27 +0700 Subject: [PATCH 041/182] change asdict order --- fastcore/xtras.py | 4 ++-- nbs/03_xtras.ipynb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 463cd9db..36dea3cd 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -722,8 +722,8 @@ def flexiclass( def asdict(o)->dict: "Convert `o` to a `dict`, supporting dataclasses, namedtuples, iterables, and `__dict__` attrs." if isinstance(o, dict): return o - if is_dataclass(o): r = dataclasses.asdict(o) - elif hasattr(o, '_asdict'): r = o._asdict() + if hasattr(o, '_asdict'): r = o._asdict() + elif is_dataclass(o): r = dataclasses.asdict(o) elif hasattr(o, '__iter__'): try: r = dict(o) except TypeError: pass diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index a34af337..f514b0e8 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -3032,8 +3032,8 @@ "def asdict(o)->dict:\n", " \"Convert `o` to a `dict`, supporting dataclasses, namedtuples, iterables, and `__dict__` attrs.\"\n", " if isinstance(o, dict): return o\n", - " if is_dataclass(o): r = dataclasses.asdict(o)\n", - " elif hasattr(o, '_asdict'): r = o._asdict()\n", + " if hasattr(o, '_asdict'): r = o._asdict()\n", + " elif is_dataclass(o): r = dataclasses.asdict(o)\n", " elif hasattr(o, '__iter__'):\n", " try: r = dict(o)\n", " except TypeError: pass\n", From 373a91d2dfed10f39a64886cca0855f9900ecc11 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 19 Jan 2025 10:37:29 +1000 Subject: [PATCH 042/182] fixes #664 --- fastcore/docments.py | 20 +++++--- nbs/06_docments.ipynb | 117 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 108 insertions(+), 29 deletions(-) diff --git a/fastcore/docments.py b/fastcore/docments.py index 02efe9ed..f68a5b3d 100644 --- a/fastcore/docments.py +++ b/fastcore/docments.py @@ -73,9 +73,9 @@ def _param_locs(s, returns=True, args_kwargs=False): if isinstance(defn, (FunctionDef, AsyncFunctionDef)): res = {arg.lineno:arg.arg for arg in defn.args.args} # Add *args if present - if defn.args.vararg and args_kwargs: res[defn.args.vararg.lineno] = defn.args.vararg.arg + if defn.args.vararg: res[defn.args.vararg.lineno] = defn.args.vararg.arg # Add keyword-only args - if args_kwargs: res.update({arg.lineno:arg.arg for arg in defn.args.kwonlyargs}) + res.update({arg.lineno:arg.arg for arg in defn.args.kwonlyargs}) # Add **kwargs if present if defn.args.kwarg and args_kwargs: res[defn.args.kwarg.lineno] = defn.args.kwarg.arg if returns and defn.returns: res[defn.returns.lineno] = 'return' @@ -98,9 +98,12 @@ def _get_comment(line, arg, comments, parms): line -= 1 return dedent('\n'.join(reversed(res))) if res else None -def _get_full(anno, name, default, docs): - if anno==empty and default!=empty: anno = type(default) - return AttrDict(docment=docs.get(name), anno=anno, default=default) +def _get_full(p, docs): + anno = p.annotation + if anno==empty: + if p.default!=empty: anno = type(p.default) + elif p.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD): anno = p.kind + return AttrDict(docment=docs.get(p.name), anno=anno, default=p.default) # %% ../nbs/06_docments.ipynb def _merge_doc(dm, npdoc): @@ -148,8 +151,8 @@ def _docments(s, returns=True, eval_str=False, args_kwargs=False): docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()} sig = signature_ex(s, True) - res = {arg:_get_full(p.annotation, p.name, p.default, docs) for arg,p in sig.parameters.items()} - if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs) + res = {name:_get_full(p, docs) for name,p in sig.parameters.items()} + if returns: res['return'] = AttrDict(docment=docs.get('return'), anno=sig.return_annotation, default=empty) res = _merge_docs(res, nps) if eval_str: hints = type_hints(s) @@ -159,8 +162,9 @@ def _docments(s, returns=True, eval_str=False, args_kwargs=False): # %% ../nbs/06_docments.ipynb @delegates(_docments) -def docments(elt, full=False, **kwargs): +def docments(elt, full=False, args_kwargs=False, **kwargs): "Generates a `docment`" + if full: args_kwargs=True r = {} params = set(signature(elt).parameters) params.add('return') diff --git a/nbs/06_docments.ipynb b/nbs/06_docments.ipynb index 1d1fd537..6c2eac6b 100644 --- a/nbs/06_docments.ipynb +++ b/nbs/06_docments.ipynb @@ -245,9 +245,9 @@ " if isinstance(defn, (FunctionDef, AsyncFunctionDef)):\n", " res = {arg.lineno:arg.arg for arg in defn.args.args}\n", " # Add *args if present\n", - " if defn.args.vararg and args_kwargs: res[defn.args.vararg.lineno] = defn.args.vararg.arg\n", + " if defn.args.vararg: res[defn.args.vararg.lineno] = defn.args.vararg.arg\n", " # Add keyword-only args\n", - " if args_kwargs: res.update({arg.lineno:arg.arg for arg in defn.args.kwonlyargs})\n", + " res.update({arg.lineno:arg.arg for arg in defn.args.kwonlyargs})\n", " # Add **kwargs if present\n", " if defn.args.kwarg and args_kwargs: res[defn.args.kwarg.lineno] = defn.args.kwarg.arg\n", " if returns and defn.returns: res[defn.returns.lineno] = 'return'\n", @@ -258,6 +258,27 @@ " return None" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{2: 'a', 3: 'b', 4: 'return'}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "parms = _param_locs(add)\n", + "parms" + ] + }, { "cell_type": "code", "execution_count": null, @@ -284,9 +305,32 @@ " line -= 1\n", " return dedent('\\n'.join(reversed(res))) if res else None\n", "\n", - "def _get_full(anno, name, default, docs):\n", - " if anno==empty and default!=empty: anno = type(default)\n", - " return AttrDict(docment=docs.get(name), anno=anno, default=default)" + "def _get_full(p, docs):\n", + " anno = p.annotation\n", + " if anno==empty:\n", + " if p.default!=empty: anno = type(p.default)\n", + " elif p.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD): anno = p.kind\n", + " return AttrDict(docment=docs.get(p.name), anno=anno, default=p.default)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'the 1st number to add'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_get_comment(2, 'a', {2: ' the 1st number to add'}, parms)" ] }, { @@ -395,8 +439,8 @@ " docs = {arg:_get_comment(line, arg, comments, parms) for line,arg in parms.items()}\n", "\n", " sig = signature_ex(s, True)\n", - " res = {arg:_get_full(p.annotation, p.name, p.default, docs) for arg,p in sig.parameters.items()}\n", - " if returns: res['return'] = _get_full(sig.return_annotation, 'return', empty, docs)\n", + " res = {name:_get_full(p, docs) for name,p in sig.parameters.items()}\n", + " if returns: res['return'] = AttrDict(docment=docs.get('return'), anno=sig.return_annotation, default=empty)\n", " res = _merge_docs(res, nps)\n", " if eval_str:\n", " hints = type_hints(s)\n", @@ -413,8 +457,9 @@ "source": [ "#|export\n", "@delegates(_docments)\n", - "def docments(elt, full=False, **kwargs):\n", + "def docments(elt, full=False, args_kwargs=False, **kwargs):\n", " \"Generates a `docment`\"\n", + " if full: args_kwargs=True\n", " r = {}\n", " params = set(signature(elt).parameters)\n", " params.add('return')\n", @@ -472,6 +517,13 @@ "docments(add)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`args_kwargs=True` adds args and kwargs docs too:" + ] + }, { "cell_type": "code", "execution_count": null, @@ -484,7 +536,7 @@ "{ 'a': 'the 1st number to add',\n", " 'args': 'some args',\n", " 'b': 'the 2nd number to add',\n", - " 'kwargs': 'Passed to the `example` function',\n", + " 'kwargs': None,\n", " 'return': 'the result of adding `a` to `b`'}\n", "```" ], @@ -492,7 +544,7 @@ "{'args': 'some args',\n", " 'a': 'the 1st number to add',\n", " 'b': 'the 2nd number to add',\n", - " 'kwargs': 'Passed to the `example` function',\n", + " 'kwargs': None,\n", " 'return': 'the result of adding `a` to `b`'}" ] }, @@ -517,7 +569,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "If you pass `full=True`, the values are `dict` of defaults, types, and docments as values. Note that the type annotation is inferred from the default value, if the annotation is empty and a default is supplied." + "If you pass `full=True`, the values are `dict` of defaults, types, and docments as values. Note that the type annotation is inferred from the default value, if the annotation is empty and a default is supplied. (Note that for `full`, `args_kwargs=True` is always set too.)" ] }, { @@ -532,19 +584,31 @@ "{ 'a': { 'anno': ,\n", " 'default': ,\n", " 'docment': 'the 1st number to add'},\n", + " 'args': { 'anno': <_ParameterKind.VAR_POSITIONAL: 2>,\n", + " 'default': ,\n", + " 'docment': 'some args'},\n", " 'b': { 'anno': ,\n", " 'default': 0,\n", " 'docment': 'the 2nd number to add'},\n", + " 'kwargs': { 'anno': <_ParameterKind.VAR_KEYWORD: 4>,\n", + " 'default': ,\n", + " 'docment': None},\n", " 'return': { 'anno': ,\n", " 'default': ,\n", " 'docment': 'the result of adding `a` to `b`'}}\n", "```" ], "text/plain": [ - "{'a': {'docment': 'the 1st number to add',\n", + "{'args': {'docment': 'some args',\n", + " 'anno': <_ParameterKind.VAR_POSITIONAL: 2>,\n", + " 'default': inspect._empty},\n", + " 'a': {'docment': 'the 1st number to add',\n", " 'anno': int,\n", " 'default': inspect._empty},\n", " 'b': {'docment': 'the 2nd number to add', 'anno': int, 'default': 0},\n", + " 'kwargs': {'docment': None,\n", + " 'anno': <_ParameterKind.VAR_KEYWORD: 4>,\n", + " 'default': inspect._empty},\n", " 'return': {'docment': 'the result of adding `a` to `b`',\n", " 'anno': int,\n", " 'default': inspect._empty}}" @@ -968,11 +1032,21 @@ "data": { "text/markdown": [ "```json\n", - "{'a': 'First', 'b': 'Second', 'return': None}\n", + "{ 'a': {'anno': , 'default': 2, 'docment': 'First'},\n", + " 'b': { 'anno': 'str',\n", + " 'default': ,\n", + " 'docment': 'Second'},\n", + " 'return': { 'anno': ,\n", + " 'default': ,\n", + " 'docment': None}}\n", "```" ], "text/plain": [ - "{'a': 'First', 'return': None, 'b': 'Second'}" + "{'a': {'docment': 'First', 'anno': int, 'default': 2},\n", + " 'return': {'docment': None,\n", + " 'anno': inspect._empty,\n", + " 'default': inspect._empty},\n", + " 'b': {'docment': 'Second', 'anno': 'str', 'default': inspect._empty}}" ] }, "execution_count": null, @@ -981,13 +1055,7 @@ } ], "source": [ - "def _a(a:int=2): return a # First\n", - "\n", - "@delegates(_a)\n", - "def _b(b:str, # Second\n", - " **kwargs): \n", - " return b, (_a(**kwargs)) \n", - "docments(_b)" + "docments(_b, full=True)" ] }, { @@ -1142,6 +1210,13 @@ "#|hide\n", "import nbdev; nbdev.nbdev_export()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From d9f9dd8be3ad09d8848df2e5b767420f92af0732 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 31 Jan 2025 13:41:26 +1000 Subject: [PATCH 043/182] fixes #665 --- fastcore/_modidx.py | 1 + fastcore/basics.py | 27 +++++++++++++++++---------- nbs/01_basics.ipynb | 45 +++++++++++++++++++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 6e9bd13a..13d844f2 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -179,6 +179,7 @@ 'fastcore.basics.otherwise': ('basics.html#otherwise', 'fastcore/basics.py'), 'fastcore.basics.partialler': ('basics.html#partialler', 'fastcore/basics.py'), 'fastcore.basics.partition': ('basics.html#partition', 'fastcore/basics.py'), + 'fastcore.basics.partition_dict': ('basics.html#partition_dict', 'fastcore/basics.py'), 'fastcore.basics.patch': ('basics.html#patch', 'fastcore/basics.py'), 'fastcore.basics.patch_property': ('basics.html#patch_property', 'fastcore/basics.py'), 'fastcore.basics.patch_to': ('basics.html#patch_to', 'fastcore/basics.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index cca2170d..10a553e2 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -11,16 +11,16 @@ 'annotations', 'anno_ret', 'signature_ex', 'union2tuple', 'argnames', 'with_cast', 'store_attr', 'attrdict', 'properties', 'camel2words', 'camel2snake', 'snake2camel', 'class2attr', 'getcallable', 'getattrs', 'hasattrs', 'setattrs', 'try_attrs', 'GetAttrBase', 'GetAttr', 'delegate_attr', 'ShowPrint', 'Int', 'Str', - 'Float', 'partition', 'flatten', 'concat', 'strcat', 'detuplify', 'replicate', 'setify', 'merge', 'range_of', - 'groupby', 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle', 'zip_cycle', 'sorted_ex', - 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first', 'last', 'only', 'nested_attr', 'nested_setdefault', - 'nested_callable', 'nested_idx', 'set_nested_idx', 'val2idx', 'uniqueify', 'loop_first_last', 'loop_first', - 'loop_last', 'first_match', 'last_match', 'fastuple', 'bind', 'mapt', 'map_ex', 'compose', 'maps', - 'partialler', 'instantiate', 'using_attr', 'copy_func', 'patch_to', 'patch', 'patch_property', 'compile_re', - 'ImportEnum', 'StrEnum', 'str_enum', 'ValEnum', 'Stateful', 'NotStr', 'PrettyString', 'even_mults', - 'num_cpus', 'add_props', 'str2bool', 'str2int', 'str2float', 'str2list', 'str2date', 'to_bool', 'to_int', - 'to_float', 'to_list', 'to_date', 'typed', 'exec_new', 'exec_import', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', - 'add', 'sub', 'mul', 'truediv', 'is_', 'is_not', 'mod'] + 'Float', 'partition', 'partition_dict', 'flatten', 'concat', 'strcat', 'detuplify', 'replicate', 'setify', + 'merge', 'range_of', 'groupby', 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle', + 'zip_cycle', 'sorted_ex', 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first', 'last', 'only', + 'nested_attr', 'nested_setdefault', 'nested_callable', 'nested_idx', 'set_nested_idx', 'val2idx', + 'uniqueify', 'loop_first_last', 'loop_first', 'loop_last', 'first_match', 'last_match', 'fastuple', 'bind', + 'mapt', 'map_ex', 'compose', 'maps', 'partialler', 'instantiate', 'using_attr', 'copy_func', 'patch_to', + 'patch', 'patch_property', 'compile_re', 'ImportEnum', 'StrEnum', 'str_enum', 'ValEnum', 'Stateful', + 'NotStr', 'PrettyString', 'even_mults', 'num_cpus', 'add_props', 'str2bool', 'str2int', 'str2float', + 'str2list', 'str2date', 'to_bool', 'to_int', 'to_float', 'to_list', 'to_date', 'typed', 'exec_new', + 'exec_import', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', 'add', 'sub', 'mul', 'truediv', 'is_', 'is_not', 'mod'] # %% ../nbs/01_basics.ipynb from .imports import * @@ -574,6 +574,13 @@ def partition(coll, f): ts,fs = typ(ts),typ(fs) return ts,fs +# %% ../nbs/01_basics.ipynb +def partition_dict(d, f): + "Partition a dict by a predicate that takes key/value params" + ts,fs = {},{} + for k,v in d.items(): (fs,ts)[f(k,v)][k] = v + return ts,fs + # %% ../nbs/01_basics.ipynb def flatten(o): "Concatenate all collections and items as a generator" diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 3b0631e7..c8ab3f99 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -672,14 +672,6 @@ "execution_count": null, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/jhoward/miniforge3/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] - }, { "data": { "text/markdown": [ @@ -3388,6 +3380,43 @@ "test_eq(ts, [1,3,5,7,9])" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def partition_dict(d, f):\n", + " \"Partition a dict by a predicate that takes key/value params\"\n", + " ts,fs = {},{}\n", + " for k,v in d.items(): (fs,ts)[f(k,v)][k] = v\n", + " return ts,fs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "d = {'a':1, 'b':2, 'c':3, 'd':4}\n", + "ts,fs = partition_dict(d, lambda k,v: v%2)\n", + "test_eq(fs, {'b':2, 'd':4})\n", + "test_eq(ts, {'a':1, 'c':3})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ts,fs = partition_dict(d, lambda k,v: k in 'bc')\n", + "test_eq(ts, {'b':2, 'c':3})\n", + "test_eq(fs, {'a':1, 'd':4})" + ] + }, { "cell_type": "code", "execution_count": null, From 6c7c6700351067cc1c1cb4ddaab7a707ab51f96b Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 31 Jan 2025 13:42:08 +1000 Subject: [PATCH 044/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074d3a95..8ae00b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.7.29 + +### New Features + +- Add `partition_dict` ([#665](https://github.com/AnswerDotAI/fastcore/issues/665)) +- Add `VAR_POSITIONAL` and `VAR_KEYWORD` to docments ([#664](https://github.com/AnswerDotAI/fastcore/issues/664)) + + ## 1.7.28 ### New Features From 7386537ee790542b6e673d4597f9f7fb938c4706 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 31 Jan 2025 13:42:31 +1000 Subject: [PATCH 045/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 351f3f64..f3ce51b7 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.7.29" +__version__ = "1.7.30" diff --git a/settings.ini b/settings.ini index dbe79182..0c104996 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = master -version = 1.7.29 +version = 1.7.30 min_python = 3.9 audience = Developers language = English From 1f3e2b4d76371a1d1c4b98355e506ca362ae56ca Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Mon, 17 Feb 2025 20:30:36 +0000 Subject: [PATCH 046/182] Add sort args to delegates --- fastcore/meta.py | 4 +++- nbs/07_meta.ipynb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/fastcore/meta.py b/fastcore/meta.py index 06c33c31..a05b51d6 100644 --- a/fastcore/meta.py +++ b/fastcore/meta.py @@ -110,7 +110,8 @@ def _f(f): # %% ../nbs/07_meta.ipynb def delegates(to:FunctionType=None, # Delegatee keep=False, # Keep `kwargs` in decorated function? - but:list=None): # Exclude these parameters from signature + but:list=None, + sort_args=False): # Exclude these parameters from signature "Decorator: replace `**kwargs` in signature with params from `to`" if but is None: but = [] def _f(f): @@ -124,6 +125,7 @@ def _f(f): k = sigd.pop('kwargs') s2 = {k:v.replace(kind=inspect.Parameter.KEYWORD_ONLY) for k,v in inspect.signature(to_f).parameters.items() if v.default != inspect.Parameter.empty and k not in sigd and k not in but} + if sort_args: s2 = dict(sorted(s2.items())) anno = {k:v for k,v in getattr(to_f, "__annotations__", {}).items() if k not in sigd and k not in but} sigd.update(s2) if keep: sigd['kwargs'] = k diff --git a/nbs/07_meta.ipynb b/nbs/07_meta.ipynb index 74996f1e..64dd426f 100644 --- a/nbs/07_meta.ipynb +++ b/nbs/07_meta.ipynb @@ -904,7 +904,8 @@ "#|export\n", "def delegates(to:FunctionType=None, # Delegatee\n", " keep=False, # Keep `kwargs` in decorated function?\n", - " but:list=None): # Exclude these parameters from signature\n", + " but:list=None,\n", + " sort_args=False): # Exclude these parameters from signature\n", " \"Decorator: replace `**kwargs` in signature with params from `to`\"\n", " if but is None: but = []\n", " def _f(f):\n", @@ -918,6 +919,7 @@ " k = sigd.pop('kwargs')\n", " s2 = {k:v.replace(kind=inspect.Parameter.KEYWORD_ONLY) for k,v in inspect.signature(to_f).parameters.items()\n", " if v.default != inspect.Parameter.empty and k not in sigd and k not in but}\n", + " if sort_args: s2 = dict(sorted(s2.items()))\n", " anno = {k:v for k,v in getattr(to_f, \"__annotations__\", {}).items() if k not in sigd and k not in but}\n", " sigd.update(s2)\n", " if keep: sigd['kwargs'] = k\n", From 79780ebb3f67d375fc3228dcc4f3853d97f21659 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Mon, 17 Feb 2025 20:59:23 +0000 Subject: [PATCH 047/182] Cleanup docment for sort_args --- fastcore/meta.py | 4 ++-- nbs/07_meta.ipynb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fastcore/meta.py b/fastcore/meta.py index a05b51d6..0c194d37 100644 --- a/fastcore/meta.py +++ b/fastcore/meta.py @@ -110,8 +110,8 @@ def _f(f): # %% ../nbs/07_meta.ipynb def delegates(to:FunctionType=None, # Delegatee keep=False, # Keep `kwargs` in decorated function? - but:list=None, - sort_args=False): # Exclude these parameters from signature + but:list=None, # Exclude these parameters from signature + sort_args=False): # Sort arguments alphabetically "Decorator: replace `**kwargs` in signature with params from `to`" if but is None: but = [] def _f(f): diff --git a/nbs/07_meta.ipynb b/nbs/07_meta.ipynb index 64dd426f..5fa86dc2 100644 --- a/nbs/07_meta.ipynb +++ b/nbs/07_meta.ipynb @@ -904,8 +904,8 @@ "#|export\n", "def delegates(to:FunctionType=None, # Delegatee\n", " keep=False, # Keep `kwargs` in decorated function?\n", - " but:list=None,\n", - " sort_args=False): # Exclude these parameters from signature\n", + " but:list=None, # Exclude these parameters from signature\n", + " sort_args=False): # Sort arguments alphabetically\n", " \"Decorator: replace `**kwargs` in signature with params from `to`\"\n", " if but is None: but = []\n", " def _f(f):\n", From 3b2af9cd00a08120a91358d67a34a7a60e26b673 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Wed, 19 Feb 2025 10:57:37 +0000 Subject: [PATCH 048/182] Add test and documentation Co-Authored-By: Audrey M. Roy Greenfeld --- fastcore/meta.py | 2 +- nbs/07_meta.ipynb | 92 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/fastcore/meta.py b/fastcore/meta.py index 0c194d37..bd338632 100644 --- a/fastcore/meta.py +++ b/fastcore/meta.py @@ -111,7 +111,7 @@ def _f(f): def delegates(to:FunctionType=None, # Delegatee keep=False, # Keep `kwargs` in decorated function? but:list=None, # Exclude these parameters from signature - sort_args=False): # Sort arguments alphabetically + sort_args=False): # Sort arguments alphabetically, doesn't work with call_parse "Decorator: replace `**kwargs` in signature with params from `to`" if but is None: but = [] def _f(f): diff --git a/nbs/07_meta.ipynb b/nbs/07_meta.ipynb index 5fa86dc2..b93a5288 100644 --- a/nbs/07_meta.ipynb +++ b/nbs/07_meta.ipynb @@ -122,20 +122,24 @@ "text/markdown": [ "---\n", "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/meta.py#L28){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### FixSigMeta\n", "\n", "> FixSigMeta (name, bases, dict)\n", "\n", - "A metaclass that fixes the signature on classes that override `__new__`" + "*A metaclass that fixes the signature on classes that override `__new__`*" ], "text/plain": [ "---\n", "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/meta.py#L28){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### FixSigMeta\n", "\n", "> FixSigMeta (name, bases, dict)\n", "\n", - "A metaclass that fixes the signature on classes that override `__new__`" + "*A metaclass that fixes the signature on classes that override `__new__`*" ] }, "execution_count": null, @@ -343,20 +347,24 @@ "text/markdown": [ "---\n", "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/meta.py#L36){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### PrePostInitMeta\n", "\n", "> PrePostInitMeta (name, bases, dict)\n", "\n", - "A metaclass that calls optional `__pre_init__` and `__post_init__` methods" + "*A metaclass that calls optional `__pre_init__` and `__post_init__` methods*" ], "text/plain": [ "---\n", "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/meta.py#L36){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### PrePostInitMeta\n", "\n", "> PrePostInitMeta (name, bases, dict)\n", "\n", - "A metaclass that calls optional `__pre_init__` and `__post_init__` methods" + "*A metaclass that calls optional `__pre_init__` and `__post_init__` methods*" ] }, "execution_count": null, @@ -460,20 +468,24 @@ "text/markdown": [ "---\n", "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/meta.py#L52){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### NewChkMeta\n", "\n", "> NewChkMeta (name, bases, dict)\n", "\n", - "Metaclass to avoid recreating object passed to constructor" + "*Metaclass to avoid recreating object passed to constructor*" ], "text/plain": [ "---\n", "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/meta.py#L52){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### NewChkMeta\n", "\n", "> NewChkMeta (name, bases, dict)\n", "\n", - "Metaclass to avoid recreating object passed to constructor" + "*Metaclass to avoid recreating object passed to constructor*" ] }, "execution_count": null, @@ -630,20 +642,24 @@ "text/markdown": [ "---\n", "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/meta.py#L60){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### BypassNewMeta\n", "\n", "> BypassNewMeta (name, bases, dict)\n", "\n", - "Metaclass: casts `x` to this class if it's of type `cls._bypass_type`" + "*Metaclass: casts `x` to this class if it's of type `cls._bypass_type`*" ], "text/plain": [ "---\n", "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/meta.py#L60){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### BypassNewMeta\n", "\n", "> BypassNewMeta (name, bases, dict)\n", "\n", - "Metaclass: casts `x` to this class if it's of type `cls._bypass_type`" + "*Metaclass: casts `x` to this class if it's of type `cls._bypass_type`*" ] }, "execution_count": null, @@ -905,7 +921,7 @@ "def delegates(to:FunctionType=None, # Delegatee\n", " keep=False, # Keep `kwargs` in decorated function?\n", " but:list=None, # Exclude these parameters from signature\n", - " sort_args=False): # Sort arguments alphabetically\n", + " sort_args=False): # Sort arguments alphabetically, doesn't work with call_parse\n", " \"Decorator: replace `**kwargs` in signature with params from `to`\"\n", " if but is None: but = []\n", " def _f(f):\n", @@ -1218,6 +1234,64 @@ "assert type(a).__name__ == 'method'" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also sort the arguments by setting the `sort_args` parameter to `True`. Here's a function with arguments not in alphabetical order." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def unsortedfunc(c=3,a=1,b=2): pass\n", + "unsortedfunc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can sort them using the `sort_args` parameter:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "@delegates(unsortedfunc, sort_args=True)\n", + "def sortedfunc(**kwargs): pass\n", + "test_sig(sortedfunc, '(*, a=1, b=2, c=3)')\n", + "sortedfunc" + ] + }, { "cell_type": "code", "execution_count": null, From b643a7f016c635d7c38d0609bd106fa059017a54 Mon Sep 17 00:00:00 2001 From: Rens Date: Mon, 3 Mar 2025 14:20:07 +0100 Subject: [PATCH 049/182] remove dispatch and transform --- fastcore/_modidx.py | 103 +-- fastcore/all.py | 2 - fastcore/dispatch.py | 219 +---- fastcore/transform.py | 242 +---- nbs/04_dispatch.ipynb | 1434 ---------------------------- nbs/05_transform.ipynb | 2003 ---------------------------------------- 6 files changed, 12 insertions(+), 3991 deletions(-) delete mode 100644 nbs/04_dispatch.ipynb delete mode 100644 nbs/05_transform.ipynb diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 13d844f2..f5a3ea27 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -224,45 +224,7 @@ 'fastcore.basics.with_cast': ('basics.html#with_cast', 'fastcore/basics.py'), 'fastcore.basics.wrap_class': ('basics.html#wrap_class', 'fastcore/basics.py'), 'fastcore.basics.zip_cycle': ('basics.html#zip_cycle', 'fastcore/basics.py')}, - 'fastcore.dispatch': { 'fastcore.dispatch.DispatchReg': ('dispatch.html#dispatchreg', 'fastcore/dispatch.py'), - 'fastcore.dispatch.DispatchReg.__call__': ('dispatch.html#dispatchreg.__call__', 'fastcore/dispatch.py'), - 'fastcore.dispatch.DispatchReg.__init__': ('dispatch.html#dispatchreg.__init__', 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch': ('dispatch.html#typedispatch', 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch.__call__': ( 'dispatch.html#typedispatch.__call__', - 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch.__get__': ('dispatch.html#typedispatch.__get__', 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch.__getitem__': ( 'dispatch.html#typedispatch.__getitem__', - 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch.__init__': ( 'dispatch.html#typedispatch.__init__', - 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch.__repr__': ( 'dispatch.html#typedispatch.__repr__', - 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch._attname': ( 'dispatch.html#typedispatch._attname', - 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch.add': ('dispatch.html#typedispatch.add', 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch.first': ('dispatch.html#typedispatch.first', 'fastcore/dispatch.py'), - 'fastcore.dispatch.TypeDispatch.returns': ('dispatch.html#typedispatch.returns', 'fastcore/dispatch.py'), - 'fastcore.dispatch._TypeDict': ('dispatch.html#_typedict', 'fastcore/dispatch.py'), - 'fastcore.dispatch._TypeDict.__getitem__': ( 'dispatch.html#_typedict.__getitem__', - 'fastcore/dispatch.py'), - 'fastcore.dispatch._TypeDict.__init__': ('dispatch.html#_typedict.__init__', 'fastcore/dispatch.py'), - 'fastcore.dispatch._TypeDict.__repr__': ('dispatch.html#_typedict.__repr__', 'fastcore/dispatch.py'), - 'fastcore.dispatch._TypeDict._reset': ('dispatch.html#_typedict._reset', 'fastcore/dispatch.py'), - 'fastcore.dispatch._TypeDict.add': ('dispatch.html#_typedict.add', 'fastcore/dispatch.py'), - 'fastcore.dispatch._TypeDict.all_matches': ( 'dispatch.html#_typedict.all_matches', - 'fastcore/dispatch.py'), - 'fastcore.dispatch._TypeDict.first': ('dispatch.html#_typedict.first', 'fastcore/dispatch.py'), - 'fastcore.dispatch._chk_defaults': ('dispatch.html#_chk_defaults', 'fastcore/dispatch.py'), - 'fastcore.dispatch._p2_anno': ('dispatch.html#_p2_anno', 'fastcore/dispatch.py'), - 'fastcore.dispatch.cast': ('dispatch.html#cast', 'fastcore/dispatch.py'), - 'fastcore.dispatch.default_set_meta': ('dispatch.html#default_set_meta', 'fastcore/dispatch.py'), - 'fastcore.dispatch.explode_types': ('dispatch.html#explode_types', 'fastcore/dispatch.py'), - 'fastcore.dispatch.lenient_issubclass': ('dispatch.html#lenient_issubclass', 'fastcore/dispatch.py'), - 'fastcore.dispatch.retain_meta': ('dispatch.html#retain_meta', 'fastcore/dispatch.py'), - 'fastcore.dispatch.retain_type': ('dispatch.html#retain_type', 'fastcore/dispatch.py'), - 'fastcore.dispatch.retain_types': ('dispatch.html#retain_types', 'fastcore/dispatch.py'), - 'fastcore.dispatch.sorted_topologically': ( 'dispatch.html#sorted_topologically', - 'fastcore/dispatch.py')}, + 'fastcore.dispatch': {}, 'fastcore.docments': { 'fastcore.docments._DocstringExtractor': ('docments.html#_docstringextractor', 'fastcore/docments.py'), 'fastcore.docments._DocstringExtractor.__init__': ( 'docments.html#_docstringextractor.__init__', 'fastcore/docments.py'), @@ -528,68 +490,7 @@ 'fastcore.test.test_shuffled': ('test.html#test_shuffled', 'fastcore/test.py'), 'fastcore.test.test_stdout': ('test.html#test_stdout', 'fastcore/test.py'), 'fastcore.test.test_warns': ('test.html#test_warns', 'fastcore/test.py')}, - 'fastcore.transform': { 'fastcore.transform.DisplayedTransform': ('transform.html#displayedtransform', 'fastcore/transform.py'), - 'fastcore.transform.DisplayedTransform.name': ( 'transform.html#displayedtransform.name', - 'fastcore/transform.py'), - 'fastcore.transform.Func': ('transform.html#func', 'fastcore/transform.py'), - 'fastcore.transform.Func.__call__': ('transform.html#func.__call__', 'fastcore/transform.py'), - 'fastcore.transform.Func.__init__': ('transform.html#func.__init__', 'fastcore/transform.py'), - 'fastcore.transform.Func.__repr__': ('transform.html#func.__repr__', 'fastcore/transform.py'), - 'fastcore.transform.Func._get': ('transform.html#func._get', 'fastcore/transform.py'), - 'fastcore.transform.InplaceTransform': ('transform.html#inplacetransform', 'fastcore/transform.py'), - 'fastcore.transform.InplaceTransform._call': ( 'transform.html#inplacetransform._call', - 'fastcore/transform.py'), - 'fastcore.transform.ItemTransform': ('transform.html#itemtransform', 'fastcore/transform.py'), - 'fastcore.transform.ItemTransform.__call__': ( 'transform.html#itemtransform.__call__', - 'fastcore/transform.py'), - 'fastcore.transform.ItemTransform._call1': ( 'transform.html#itemtransform._call1', - 'fastcore/transform.py'), - 'fastcore.transform.ItemTransform.decode': ( 'transform.html#itemtransform.decode', - 'fastcore/transform.py'), - 'fastcore.transform.Pipeline': ('transform.html#pipeline', 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.__call__': ('transform.html#pipeline.__call__', 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.__dir__': ('transform.html#pipeline.__dir__', 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.__getattr__': ( 'transform.html#pipeline.__getattr__', - 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.__getitem__': ( 'transform.html#pipeline.__getitem__', - 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.__init__': ('transform.html#pipeline.__init__', 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.__repr__': ('transform.html#pipeline.__repr__', 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.__setstate__': ( 'transform.html#pipeline.__setstate__', - 'fastcore/transform.py'), - 'fastcore.transform.Pipeline._is_showable': ( 'transform.html#pipeline._is_showable', - 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.add': ('transform.html#pipeline.add', 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.decode': ('transform.html#pipeline.decode', 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.setup': ('transform.html#pipeline.setup', 'fastcore/transform.py'), - 'fastcore.transform.Pipeline.show': ('transform.html#pipeline.show', 'fastcore/transform.py'), - 'fastcore.transform.Transform': ('transform.html#transform', 'fastcore/transform.py'), - 'fastcore.transform.Transform.__call__': ('transform.html#transform.__call__', 'fastcore/transform.py'), - 'fastcore.transform.Transform.__init__': ('transform.html#transform.__init__', 'fastcore/transform.py'), - 'fastcore.transform.Transform.__repr__': ('transform.html#transform.__repr__', 'fastcore/transform.py'), - 'fastcore.transform.Transform._call': ('transform.html#transform._call', 'fastcore/transform.py'), - 'fastcore.transform.Transform._do_call': ('transform.html#transform._do_call', 'fastcore/transform.py'), - 'fastcore.transform.Transform.decode': ('transform.html#transform.decode', 'fastcore/transform.py'), - 'fastcore.transform.Transform.name': ('transform.html#transform.name', 'fastcore/transform.py'), - 'fastcore.transform.Transform.setup': ('transform.html#transform.setup', 'fastcore/transform.py'), - 'fastcore.transform._Sig': ('transform.html#_sig', 'fastcore/transform.py'), - 'fastcore.transform._Sig.__getattr__': ('transform.html#_sig.__getattr__', 'fastcore/transform.py'), - 'fastcore.transform._TfmDict': ('transform.html#_tfmdict', 'fastcore/transform.py'), - 'fastcore.transform._TfmDict.__setitem__': ( 'transform.html#_tfmdict.__setitem__', - 'fastcore/transform.py'), - 'fastcore.transform._TfmMeta': ('transform.html#_tfmmeta', 'fastcore/transform.py'), - 'fastcore.transform._TfmMeta.__call__': ('transform.html#_tfmmeta.__call__', 'fastcore/transform.py'), - 'fastcore.transform._TfmMeta.__new__': ('transform.html#_tfmmeta.__new__', 'fastcore/transform.py'), - 'fastcore.transform._TfmMeta.__prepare__': ( 'transform.html#_tfmmeta.__prepare__', - 'fastcore/transform.py'), - 'fastcore.transform._get_name': ('transform.html#_get_name', 'fastcore/transform.py'), - 'fastcore.transform._is_tfm_method': ('transform.html#_is_tfm_method', 'fastcore/transform.py'), - 'fastcore.transform._is_tuple': ('transform.html#_is_tuple', 'fastcore/transform.py'), - 'fastcore.transform.compose_tfms': ('transform.html#compose_tfms', 'fastcore/transform.py'), - 'fastcore.transform.gather_attr_names': ('transform.html#gather_attr_names', 'fastcore/transform.py'), - 'fastcore.transform.gather_attrs': ('transform.html#gather_attrs', 'fastcore/transform.py'), - 'fastcore.transform.get_func': ('transform.html#get_func', 'fastcore/transform.py'), - 'fastcore.transform.mk_transform': ('transform.html#mk_transform', 'fastcore/transform.py')}, + 'fastcore.transform': {}, 'fastcore.utils': {}, 'fastcore.xdg': { 'fastcore.xdg._path_from_env': ('xdg.html#_path_from_env', 'fastcore/xdg.py'), 'fastcore.xdg._paths_from_env': ('xdg.html#_paths_from_env', 'fastcore/xdg.py'), diff --git a/fastcore/all.py b/fastcore/all.py index 1a9735ce..99babb3a 100644 --- a/fastcore/all.py +++ b/fastcore/all.py @@ -1,10 +1,8 @@ from .imports import * from .foundation import * -from .dispatch import * from .utils import * from .parallel import * from .net import * -from .transform import * from .test import * from .meta import * from .imports import * diff --git a/fastcore/dispatch.py b/fastcore/dispatch.py index f11a386b..6ca21a66 100644 --- a/fastcore/dispatch.py +++ b/fastcore/dispatch.py @@ -1,214 +1,5 @@ -"""Basic single and dual parameter dispatch""" - -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_dispatch.ipynb. - -# %% ../nbs/04_dispatch.ipynb 1 -from __future__ import annotations -from .imports import * -from .foundation import * -from .utils import * - -from collections import defaultdict - -# %% auto 0 -__all__ = ['typedispatch', 'lenient_issubclass', 'sorted_topologically', 'TypeDispatch', 'DispatchReg', 'retain_meta', - 'default_set_meta', 'cast', 'retain_type', 'retain_types', 'explode_types'] - -# %% ../nbs/04_dispatch.ipynb -def lenient_issubclass(cls, types): - "If possible return whether `cls` is a subclass of `types`, otherwise return False." - if cls is object and types is not object: return False # treat `object` as highest level - try: return isinstance(cls, types) or issubclass(cls, types) - except: return False - -# %% ../nbs/04_dispatch.ipynb -def sorted_topologically(iterable, *, cmp=operator.lt, reverse=False): - "Return a new list containing all items from the iterable sorted topologically" - l,res = L(list(iterable)),[] - for _ in range(len(l)): - t = l.reduce(lambda x,y: y if cmp(y,x) else x) - res.append(t), l.remove(t) - return res[::-1] if reverse else res - -# %% ../nbs/04_dispatch.ipynb -def _chk_defaults(f, ann): - pass -# Implementation removed until we can figure out how to do this without `inspect` module -# try: # Some callables don't have signatures, so ignore those errors -# params = list(inspect.signature(f).parameters.values())[:min(len(ann),2)] -# if any(p.default!=inspect.Parameter.empty for p in params): -# warn(f"{f.__name__} has default params. These will be ignored.") -# except ValueError: pass - -# %% ../nbs/04_dispatch.ipynb -def _p2_anno(f): - "Get the 1st 2 annotations of `f`, defaulting to `object`" - hints = type_hints(f) - ann = [o for n,o in hints.items() if n!='return'] - if callable(f): _chk_defaults(f, ann) - while len(ann)<2: ann.append(object) - return ann[:2] - -# %% ../nbs/04_dispatch.ipynb -class _TypeDict: - def __init__(self): self.d,self.cache = {},{} - - def _reset(self): - self.d = {k:self.d[k] for k in sorted_topologically(self.d, cmp=lenient_issubclass)} - self.cache = {} - - def add(self, t, f): - "Add type `t` and function `f`" - if not isinstance(t, tuple): t = tuple(L(union2tuple(t))) - for t_ in t: self.d[t_] = f - self._reset() - - def all_matches(self, k): - "Find first matching type that is a super-class of `k`" - if k not in self.cache: - types = [f for f in self.d if lenient_issubclass(k,f)] - self.cache[k] = [self.d[o] for o in types] - return self.cache[k] - - def __getitem__(self, k): - "Find first matching type that is a super-class of `k`" - res = self.all_matches(k) - return res[0] if len(res) else None - - def __repr__(self): return self.d.__repr__() - def first(self): return first(self.d.values()) - -# %% ../nbs/04_dispatch.ipynb -class TypeDispatch: - "Dictionary-like object; `__getitem__` matches keys of types using `issubclass`" - def __init__(self, funcs=(), bases=()): - self.funcs,self.bases = _TypeDict(),L(bases).filter(is_not(None)) - for o in L(funcs): self.add(o) - self.inst = None - self.owner = None - - def add(self, f): - "Add type `t` and function `f`" - if isinstance(f, staticmethod): a0,a1 = _p2_anno(f.__func__) - else: a0,a1 = _p2_anno(f) - t = self.funcs.d.get(a0) - if t is None: - t = _TypeDict() - self.funcs.add(a0, t) - t.add(a1, f) - - def first(self): - "Get first function in ordered dict of type:func." - return self.funcs.first().first() - - def returns(self, x): - "Get the return type of annotation of `x`." - return anno_ret(self[type(x)]) - - def _attname(self,k): return getattr(k,'__name__',str(k)) - def __repr__(self): - r = [f'({self._attname(k)},{self._attname(l)}) -> {getattr(v, "__name__", type(v).__name__)}' - for k in self.funcs.d for l,v in self.funcs[k].d.items()] - r = r + [o.__repr__() for o in self.bases] - return '\n'.join(r) - - def __call__(self, *args, **kwargs): - ts = L(args).map(type)[:2] - f = self[tuple(ts)] - if not f: return args[0] - if isinstance(f, staticmethod): f = f.__func__ - elif self.inst is not None: f = MethodType(f, self.inst) - elif self.owner is not None: f = MethodType(f, self.owner) - return f(*args, **kwargs) - - def __get__(self, inst, owner): - self.inst = inst - self.owner = owner - return self - - def __getitem__(self, k): - "Find first matching type that is a super-class of `k`" - k = L(k) - while len(k)<2: k.append(object) - r = self.funcs.all_matches(k[0]) - for t in r: - o = t[k[1]] - if o is not None: return o - for base in self.bases: - res = base[k] - if res is not None: return res - return None - -# %% ../nbs/04_dispatch.ipynb -class DispatchReg: - "A global registry for `TypeDispatch` objects keyed by function name" - def __init__(self): self.d = defaultdict(TypeDispatch) - def __call__(self, f): - if isinstance(f, (classmethod, staticmethod)): nm = f'{f.__func__.__qualname__}' - else: nm = f'{f.__qualname__}' - if isinstance(f, classmethod): f=f.__func__ - self.d[nm].add(f) - return self.d[nm] - -typedispatch = DispatchReg() - -# %% ../nbs/04_dispatch.ipynb -_all_=['cast'] - -# %% ../nbs/04_dispatch.ipynb -def retain_meta(x, res, as_copy=False): - "Call `res.set_meta(x)`, if it exists" - if hasattr(res,'set_meta'): res.set_meta(x, as_copy=as_copy) - return res - -# %% ../nbs/04_dispatch.ipynb -def default_set_meta(self, x, as_copy=False): - "Copy over `_meta` from `x` to `res`, if it's missing" - if hasattr(x, '_meta') and not hasattr(self, '_meta'): - meta = x._meta - if as_copy: meta = copy(meta) - self._meta = meta - return self - -# %% ../nbs/04_dispatch.ipynb -@typedispatch -def cast(x, typ): - "cast `x` to type `typ` (may also change `x` inplace)" - res = typ._before_cast(x) if hasattr(typ, '_before_cast') else x - if risinstance('ndarray', res): res = res.view(typ) - elif hasattr(res, 'as_subclass'): res = res.as_subclass(typ) - else: - try: res.__class__ = typ - except: res = typ(res) - return retain_meta(x, res) - -# %% ../nbs/04_dispatch.ipynb -def retain_type(new, old=None, typ=None, as_copy=False): - "Cast `new` to type of `old` or `typ` if it's a superclass" - # e.g. old is TensorImage, new is Tensor - if not subclass then do nothing - if new is None: return - assert old is not None or typ is not None - if typ is None: - if not isinstance(old, type(new)): return new - typ = old if isinstance(old,type) else type(old) - # Do nothing the new type is already an instance of requested type (i.e. same type) - if typ==NoneType or isinstance(new, typ): return new - return retain_meta(old, cast(new, typ), as_copy=as_copy) - -# %% ../nbs/04_dispatch.ipynb -def retain_types(new, old=None, typs=None): - "Cast each item of `new` to type of matching item in `old` if it's a superclass" - if not is_listy(new): return retain_type(new, old, typs) - if typs is not None: - if isinstance(typs, dict): - t = first(typs.keys()) - typs = typs[t] - else: t,typs = typs,None - else: t = type(old) if old is not None and isinstance(old,type(new)) else type(new) - return t(L(new, old, typs).map_zip(retain_types, cycled=True)) - -# %% ../nbs/04_dispatch.ipynb -def explode_types(o): - "Return the type of `o`, potentially in nested dictionaries for thing that are listy" - if not is_listy(o): return type(o) - return {type(o): [explode_types(o_) for o_ in o]} +def __getattr__(name): + raise ImportError( + f"Could not import '{name}' from fastcore.dispatch - this module has been moved to the fasttransform package.\n" + "To migrate your code, please see the migration guide at: https://answerdotai.github.io/fasttransform/fastcore_migration_guide.html" + ) \ No newline at end of file diff --git a/fastcore/transform.py b/fastcore/transform.py index 33ebc876..f4f99bc6 100644 --- a/fastcore/transform.py +++ b/fastcore/transform.py @@ -1,237 +1,5 @@ -"""Definition of `Transform` and `Pipeline`""" - -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_transform.ipynb. - -# %% auto 0 -__all__ = ['Sig', 'Transform', 'InplaceTransform', 'DisplayedTransform', 'ItemTransform', 'get_func', 'Func', 'compose_tfms', - 'mk_transform', 'gather_attrs', 'gather_attr_names', 'Pipeline'] - -# %% ../nbs/05_transform.ipynb -from .imports import * -from .foundation import * -from .utils import * -from .dispatch import * -import inspect - -# %% ../nbs/05_transform.ipynb -_tfm_methods = 'encodes','decodes','setups' - -def _is_tfm_method(n, f): return n in _tfm_methods and callable(f) - -class _TfmDict(dict): - def __setitem__(self, k, v): - if not _is_tfm_method(k, v): return super().__setitem__(k,v) - if k not in self: super().__setitem__(k,TypeDispatch()) - self[k].add(v) - -# %% ../nbs/05_transform.ipynb -class _TfmMeta(type): - def __new__(cls, name, bases, dict): - res = super().__new__(cls, name, bases, dict) - for nm in _tfm_methods: - base_td = [getattr(b,nm,None) for b in bases] - if nm in res.__dict__: getattr(res,nm).bases = base_td - else: setattr(res, nm, TypeDispatch(bases=base_td)) - # _TfmMeta.__call__ shadows the signature of inheriting classes, set it back - res.__signature__ = inspect.signature(res.__init__) - return res - - def __call__(cls, *args, **kwargs): - f = first(args) - n = getattr(f, '__name__', None) - if _is_tfm_method(n, f): - getattr(cls,n).add(f) - return f - obj = super().__call__(*args, **kwargs) - # _TfmMeta.__new__ replaces cls.__signature__ which breaks the signature of a callable - # instances of cls, fix it - if hasattr(obj, '__call__'): obj.__signature__ = inspect.signature(obj.__call__) - return obj - - @classmethod - def __prepare__(cls, name, bases): return _TfmDict() - -# %% ../nbs/05_transform.ipynb -def _get_name(o): - if hasattr(o,'__qualname__'): return o.__qualname__ - if hasattr(o,'__name__'): return o.__name__ - return o.__class__.__name__ - -# %% ../nbs/05_transform.ipynb -def _is_tuple(o): return isinstance(o, tuple) and not hasattr(o, '_fields') - -# %% ../nbs/05_transform.ipynb -class Transform(metaclass=_TfmMeta): - "Delegates (`__call__`,`decode`,`setup`) to (encodes,decodes,setups) if `split_idx` matches" - split_idx,init_enc,order,train_setup = None,None,0,None - def __init__(self, enc=None, dec=None, split_idx=None, order=None): - self.split_idx = ifnone(split_idx, self.split_idx) - if order is not None: self.order=order - self.init_enc = enc or dec - if not self.init_enc: return - - self.encodes,self.decodes,self.setups = TypeDispatch(),TypeDispatch(),TypeDispatch() - if enc: - self.encodes.add(enc) - self.order = getattr(enc,'order',self.order) - if len(type_hints(enc)) > 0: self.input_types = union2tuple(first(type_hints(enc).values())) - self._name = _get_name(enc) - if dec: self.decodes.add(dec) - - @property - def name(self): return getattr(self, '_name', _get_name(self)) - def __call__(self, x, **kwargs): return self._call('encodes', x, **kwargs) - def decode (self, x, **kwargs): return self._call('decodes', x, **kwargs) - def __repr__(self): return f'{self.name}:\nencodes: {self.encodes}decodes: {self.decodes}' - - def setup(self, items=None, train_setup=False): - train_setup = train_setup if self.train_setup is None else self.train_setup - return self.setups(getattr(items, 'train', items) if train_setup else items) - - def _call(self, fn, x, split_idx=None, **kwargs): - if split_idx!=self.split_idx and self.split_idx is not None: return x - return self._do_call(getattr(self, fn), x, **kwargs) - - def _do_call(self, f, x, **kwargs): - if not _is_tuple(x): - if f is None: return x - ret = f.returns(x) if hasattr(f,'returns') else None - return retain_type(f(x, **kwargs), x, ret) - res = tuple(self._do_call(f, x_, **kwargs) for x_ in x) - return retain_type(res, x) - -add_docs(Transform, decode="Delegate to decodes to undo transform", setup="Delegate to setups to set up transform") - -# %% ../nbs/05_transform.ipynb -class InplaceTransform(Transform): - "A `Transform` that modifies in-place and just returns whatever it's passed" - def _call(self, fn, x, split_idx=None, **kwargs): - super()._call(fn,x,split_idx,**kwargs) - return x - -# %% ../nbs/05_transform.ipynb -class DisplayedTransform(Transform): - "A transform with a `__repr__` that shows its attrs" - - @property - def name(self): return f"{super().name} -- {getattr(self,'__stored_args__',{})}" - -# %% ../nbs/05_transform.ipynb -class ItemTransform(Transform): - "A transform that always take tuples as items" - _retain = True - def __call__(self, x, **kwargs): return self._call1(x, '__call__', **kwargs) - def decode(self, x, **kwargs): return self._call1(x, 'decode', **kwargs) - def _call1(self, x, name, **kwargs): - if not _is_tuple(x): return getattr(super(), name)(x, **kwargs) - y = getattr(super(), name)(list(x), **kwargs) - if not self._retain: return y - if is_listy(y) and not isinstance(y, tuple): y = tuple(y) - return retain_type(y, x) - -# %% ../nbs/05_transform.ipynb -def get_func(t, name, *args, **kwargs): - "Get the `t.name` (potentially partial-ized with `args` and `kwargs`) or `noop` if not defined" - f = nested_callable(t, name) - return f if not (args or kwargs) else partial(f, *args, **kwargs) - -# %% ../nbs/05_transform.ipynb -class Func(): - "Basic wrapper around a `name` with `args` and `kwargs` to call on a given type" - def __init__(self, name, *args, **kwargs): self.name,self.args,self.kwargs = name,args,kwargs - def __repr__(self): return f'sig: {self.name}({self.args}, {self.kwargs})' - def _get(self, t): return get_func(t, self.name, *self.args, **self.kwargs) - def __call__(self,t): return mapped(self._get, t) - -# %% ../nbs/05_transform.ipynb -class _Sig(): - def __getattr__(self,k): - def _inner(*args, **kwargs): return Func(k, *args, **kwargs) - return _inner - -Sig = _Sig() - -# %% ../nbs/05_transform.ipynb -def compose_tfms(x, tfms, is_enc=True, reverse=False, **kwargs): - "Apply all `func_nm` attribute of `tfms` on `x`, maybe in `reverse` order" - if reverse: tfms = reversed(tfms) - for f in tfms: - if not is_enc: f = f.decode - x = f(x, **kwargs) - return x - -# %% ../nbs/05_transform.ipynb -def mk_transform(f): - "Convert function `f` to `Transform` if it isn't already one" - f = instantiate(f) - return f if isinstance(f,(Transform,Pipeline)) else Transform(f) - -# %% ../nbs/05_transform.ipynb -def gather_attrs(o, k, nm): - "Used in __getattr__ to collect all attrs `k` from `self.{nm}`" - if k.startswith('_') or k==nm: raise AttributeError(k) - att = getattr(o,nm) - res = [t for t in att.attrgot(k) if t is not None] - if not res: raise AttributeError(k) - return res[0] if len(res)==1 else L(res) - -# %% ../nbs/05_transform.ipynb -def gather_attr_names(o, nm): - "Used in __dir__ to collect all attrs `k` from `self.{nm}`" - return L(getattr(o,nm)).map(dir).concat().unique() - -# %% ../nbs/05_transform.ipynb -class Pipeline: - "A pipeline of composed (for encode/decode) transforms, setup with types" - def __init__(self, funcs=None, split_idx=None): - self.split_idx,self.default = split_idx,None - if funcs is None: funcs = [] - if isinstance(funcs, Pipeline): self.fs = funcs.fs - else: - if isinstance(funcs, Transform): funcs = [funcs] - self.fs = L(ifnone(funcs,[noop])).map(mk_transform).sorted(key='order') - for f in self.fs: - name = camel2snake(type(f).__name__) - a = getattr(self,name,None) - if a is not None: f = L(a)+f - setattr(self, name, f) - - def setup(self, items=None, train_setup=False): - tfms = self.fs[:] - self.fs.clear() - for t in tfms: self.add(t,items, train_setup) - - def add(self,ts, items=None, train_setup=False): - if not is_listy(ts): ts=[ts] - for t in ts: t.setup(items, train_setup) - self.fs+=ts - self.fs = self.fs.sorted(key='order') - - def __call__(self, o): return compose_tfms(o, tfms=self.fs, split_idx=self.split_idx) - def __repr__(self): return f"Pipeline: {' -> '.join([f.name for f in self.fs if f.name != 'noop'])}" - def __getitem__(self,i): return self.fs[i] - def __setstate__(self,data): self.__dict__.update(data) - def __getattr__(self,k): return gather_attrs(self, k, 'fs') - def __dir__(self): return super().__dir__() + gather_attr_names(self, 'fs') - - def decode (self, o, full=True): - if full: return compose_tfms(o, tfms=self.fs, is_enc=False, reverse=True, split_idx=self.split_idx) - #Not full means we decode up to the point the item knows how to show itself. - for f in reversed(self.fs): - if self._is_showable(o): return o - o = f.decode(o, split_idx=self.split_idx) - return o - - def show(self, o, ctx=None, **kwargs): - o = self.decode(o, full=False) - o1 = (o,) if not _is_tuple(o) else o - if hasattr(o, 'show'): ctx = o.show(ctx=ctx, **kwargs) - else: - for o_ in o1: - if hasattr(o_, 'show'): ctx = o_.show(ctx=ctx, **kwargs) - return ctx - - def _is_showable(self, o): - if hasattr(o, 'show'): return True - if _is_tuple(o): return all(hasattr(o_, 'show') for o_ in o) - return False +def __getattr__(name): + raise ImportError( + f"Could not import '{name}' from fastcore.transform - this module has been moved to the fasttransform package.\n" + "To migrate your code, please see the migration guide at: https://answerdotai.github.io/fasttransform/fastcore_migration_guide.html" + ) \ No newline at end of file diff --git a/nbs/04_dispatch.ipynb b/nbs/04_dispatch.ipynb deleted file mode 100644 index 9783d432..00000000 --- a/nbs/04_dispatch.ipynb +++ /dev/null @@ -1,1434 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|default_exp dispatch" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "from __future__ import annotations\n", - "from fastcore.imports import *\n", - "from fastcore.foundation import *\n", - "from fastcore.utils import *\n", - "\n", - "from collections import defaultdict" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from nbdev.showdoc import *\n", - "from fastcore.test import *\n", - "from fastcore.nb_imports import *" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Type dispatch\n", - "\n", - "> Basic single and dual parameter dispatch" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Helpers" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def lenient_issubclass(cls, types):\n", - " \"If possible return whether `cls` is a subclass of `types`, otherwise return False.\"\n", - " if cls is object and types is not object: return False # treat `object` as highest level\n", - " try: return isinstance(cls, types) or issubclass(cls, types)\n", - " except: return False" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "assert not lenient_issubclass(typing.Collection, list)\n", - "assert lenient_issubclass(list, typing.Collection)\n", - "assert lenient_issubclass(typing.Collection, object)\n", - "assert lenient_issubclass(typing.List, typing.Collection)\n", - "assert not lenient_issubclass(typing.Collection, typing.List)\n", - "assert not lenient_issubclass(object, typing.Callable)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def sorted_topologically(iterable, *, cmp=operator.lt, reverse=False):\n", - " \"Return a new list containing all items from the iterable sorted topologically\"\n", - " l,res = L(list(iterable)),[]\n", - " for _ in range(len(l)):\n", - " t = l.reduce(lambda x,y: y if cmp(y,x) else x)\n", - " res.append(t), l.remove(t)\n", - " return res[::-1] if reverse else res" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "td = [3, 1, 2, 5]\n", - "test_eq(sorted_topologically(td), [1, 2, 3, 5])\n", - "test_eq(sorted_topologically(td, reverse=True), [5, 3, 2, 1])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "td = {int:1, numbers.Number:2, numbers.Integral:3}\n", - "test_eq(sorted_topologically(td, cmp=lenient_issubclass), [int, numbers.Integral, numbers.Number])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "td = [numbers.Integral, tuple, list, int, dict]\n", - "td = sorted_topologically(td, cmp=lenient_issubclass)\n", - "assert td.index(int) < td.index(numbers.Integral)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def _chk_defaults(f, ann):\n", - " pass\n", - "# Implementation removed until we can figure out how to do this without `inspect` module\n", - "# try: # Some callables don't have signatures, so ignore those errors\n", - "# params = list(inspect.signature(f).parameters.values())[:min(len(ann),2)]\n", - "# if any(p.default!=inspect.Parameter.empty for p in params):\n", - "# warn(f\"{f.__name__} has default params. These will be ignored.\")\n", - "# except ValueError: pass" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def _p2_anno(f):\n", - " \"Get the 1st 2 annotations of `f`, defaulting to `object`\"\n", - " hints = type_hints(f)\n", - " ann = [o for n,o in hints.items() if n!='return']\n", - " if callable(f): _chk_defaults(f, ann)\n", - " while len(ann)<2: ann.append(object)\n", - " return ann[:2]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "def _f(a): pass\n", - "test_eq(_p2_anno(_f), (object,object))\n", - "def _f(a, b): pass\n", - "test_eq(_p2_anno(_f), (object,object))\n", - "def _f(a:None, b)->str: pass\n", - "test_eq(_p2_anno(_f), (NoneType,object))\n", - "def _f(a:str, b)->float: pass\n", - "test_eq(_p2_anno(_f), (str,object))\n", - "def _f(a:None, b:str)->float: pass\n", - "test_eq(_p2_anno(_f), (NoneType,str))\n", - "def _f(a:int, b:int)->float: pass\n", - "test_eq(_p2_anno(_f), (int,int))\n", - "def _f(self, a:int, b:int): pass\n", - "test_eq(_p2_anno(_f), (int,int))\n", - "def _f(a:int, b:str)->float: pass\n", - "test_eq(_p2_anno(_f), (int,str))\n", - "test_eq(_p2_anno(attrgetter('foo')), (object,object))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "([object, object], [int, object])" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#|hide\n", - "# Disabled until _chk_defaults fixed\n", - "# def _f(x:int,y:int=10): pass\n", - "# test_warns(lambda: _p2_anno(_f))\n", - "def _f(x:int,y=10): pass\n", - "_p2_anno(None),_p2_anno(_f)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## TypeDispatch" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Type dispatch, or [Multiple dispatch](https://en.wikipedia.org/wiki/Multiple_dispatch#Julia), allows you to change the way a function behaves based upon the input types it recevies. This is a prominent feature in some programming languages like Julia. For example, this is a [conceptual example](https://en.wikipedia.org/wiki/Multiple_dispatch#Julia) of how multiple dispatch works in Julia, returning different values depending on the input types of x and y:\n", - "\n", - "```julia\n", - "collide_with(x::Asteroid, y::Asteroid) = ... \n", - "# deal with asteroid hitting asteroid\n", - "\n", - "collide_with(x::Asteroid, y::Spaceship) = ... \n", - "# deal with asteroid hitting spaceship\n", - "\n", - "collide_with(x::Spaceship, y::Asteroid) = ... \n", - "# deal with spaceship hitting asteroid\n", - "\n", - "collide_with(x::Spaceship, y::Spaceship) = ... \n", - "# deal with spaceship hitting spaceship\n", - "```\n", - "\n", - "Type dispatch can be especially useful in data science, where you might allow different input types (i.e. numpy arrays and pandas dataframes) to function that processes data. Type dispatch allows you to have a common API for functions that do similar tasks.\n", - "\n", - "The `TypeDispatch` class allows us to achieve type dispatch in Python. It contains a dictionary that maps types from type annotations to functions, which ensures that the proper function is called when passed inputs." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class _TypeDict:\n", - " def __init__(self): self.d,self.cache = {},{}\n", - "\n", - " def _reset(self):\n", - " self.d = {k:self.d[k] for k in sorted_topologically(self.d, cmp=lenient_issubclass)}\n", - " self.cache = {}\n", - "\n", - " def add(self, t, f):\n", - " \"Add type `t` and function `f`\"\n", - " if not isinstance(t, tuple): t = tuple(L(union2tuple(t)))\n", - " for t_ in t: self.d[t_] = f\n", - " self._reset()\n", - "\n", - " def all_matches(self, k):\n", - " \"Find first matching type that is a super-class of `k`\"\n", - " if k not in self.cache:\n", - " types = [f for f in self.d if lenient_issubclass(k,f)]\n", - " self.cache[k] = [self.d[o] for o in types]\n", - " return self.cache[k]\n", - "\n", - " def __getitem__(self, k):\n", - " \"Find first matching type that is a super-class of `k`\"\n", - " res = self.all_matches(k)\n", - " return res[0] if len(res) else None\n", - "\n", - " def __repr__(self): return self.d.__repr__()\n", - " def first(self): return first(self.d.values())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class TypeDispatch:\n", - " \"Dictionary-like object; `__getitem__` matches keys of types using `issubclass`\"\n", - " def __init__(self, funcs=(), bases=()):\n", - " self.funcs,self.bases = _TypeDict(),L(bases).filter(is_not(None))\n", - " for o in L(funcs): self.add(o)\n", - " self.inst = None\n", - " self.owner = None\n", - "\n", - " def add(self, f):\n", - " \"Add type `t` and function `f`\"\n", - " if isinstance(f, staticmethod): a0,a1 = _p2_anno(f.__func__)\n", - " else: a0,a1 = _p2_anno(f)\n", - " t = self.funcs.d.get(a0)\n", - " if t is None:\n", - " t = _TypeDict()\n", - " self.funcs.add(a0, t)\n", - " t.add(a1, f)\n", - "\n", - " def first(self):\n", - " \"Get first function in ordered dict of type:func.\"\n", - " return self.funcs.first().first()\n", - "\n", - " def returns(self, x):\n", - " \"Get the return type of annotation of `x`.\"\n", - " return anno_ret(self[type(x)])\n", - "\n", - " def _attname(self,k): return getattr(k,'__name__',str(k))\n", - " def __repr__(self):\n", - " r = [f'({self._attname(k)},{self._attname(l)}) -> {getattr(v, \"__name__\", type(v).__name__)}'\n", - " for k in self.funcs.d for l,v in self.funcs[k].d.items()]\n", - " r = r + [o.__repr__() for o in self.bases]\n", - " return '\\n'.join(r)\n", - "\n", - " def __call__(self, *args, **kwargs):\n", - " ts = L(args).map(type)[:2]\n", - " f = self[tuple(ts)]\n", - " if not f: return args[0]\n", - " if isinstance(f, staticmethod): f = f.__func__\n", - " elif self.inst is not None: f = MethodType(f, self.inst)\n", - " elif self.owner is not None: f = MethodType(f, self.owner)\n", - " return f(*args, **kwargs)\n", - "\n", - " def __get__(self, inst, owner):\n", - " self.inst = inst\n", - " self.owner = owner\n", - " return self\n", - "\n", - " def __getitem__(self, k):\n", - " \"Find first matching type that is a super-class of `k`\"\n", - " k = L(k)\n", - " while len(k)<2: k.append(object)\n", - " r = self.funcs.all_matches(k[0])\n", - " for t in r:\n", - " o = t[k[1]]\n", - " if o is not None: return o\n", - " for base in self.bases:\n", - " res = base[k]\n", - " if res is not None: return res\n", - " return None" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To demonstrate how `TypeDispatch` works, we define a set of functions that accept a variety of input types, specified with different type annotations:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f2(x:int, y:float): return x+y #int and float for 2nd arg\n", - "def f_nin(x:numbers.Integral)->int: return x+1 #integral numeric\n", - "def f_ni2(x:int): return x #integer\n", - "def f_bll(x:bool|list): return x #bool or list\n", - "def f_num(x:numbers.Number): return x #Number (root of numerics) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can optionally initialize `TypeDispatch` with a list of functions we want to search. Printing an instance of `TypeDispatch` will display convenient mapping of types -> functions:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(bool,object) -> f_bll\n", - "(int,object) -> f_ni2\n", - "(Integral,object) -> f_nin\n", - "(Number,object) -> f_num\n", - "(list,object) -> f_bll\n", - "(object,object) -> NoneType" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "t = TypeDispatch([f_nin,f_ni2,f_num,f_bll,None])\n", - "t" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that only the first two arguments are used for `TypeDispatch`. If your function only contains one argument, the second parameter will be shown as `object`. If you pass `None` into `TypeDispatch`, then this will be displayed as `(object, object) -> NoneType`.\n", - "\n", - "`TypeDispatch` is a dictionary-like object, which means that you can retrieve a function by the associated type annotation. For example, the statement:\n", - "\n", - "```py\n", - "t[float]\n", - "```\n", - "Will return `f_num` because that is the matching function that has a type annotation that is a super-class of of `float` - `numbers.Number`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "assert issubclass(float, numbers.Number)\n", - "test_eq(t[float], f_num)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The same is true for other types as well:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(t[np.int32], f_nin)\n", - "test_eq(t[bool], f_bll)\n", - "test_eq(t[list], f_bll)\n", - "test_eq(t[np.int32], f_nin)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you try to get a type that doesn't match, `TypeDispatch` will return `None`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(t[str], None)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "

TypeDispatch.add[source]

\n", - "\n", - "> TypeDispatch.add(**`f`**)\n", - "\n", - "Add type `t` and function `f`" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "show_doc(TypeDispatch.add)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This method allows you to add an additional function to an existing `TypeDispatch` instance :" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(bool,object) -> f_bll\n", - "(int,object) -> f_ni2\n", - "(Integral,object) -> f_nin\n", - "(Number,object) -> f_num\n", - "(list,object) -> f_bll\n", - "(typing.Collection,object) -> f_col\n", - "(object,object) -> NoneType" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def f_col(x:typing.Collection): return x\n", - "t.add(f_col)\n", - "test_eq(t[str], f_col)\n", - "t" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you accidentally add the same function more than once things will still work as expected:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "t.add(f_ni2) \n", - "test_eq(t[int], f_ni2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, if you add a function that has a type collision that raises an ambiguity, this will automatically resolve to the latest function added:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f_ni3(z:int): return z # collides with f_ni2 with same type annotations\n", - "t.add(f_ni3) \n", - "test_eq(t[int], f_ni3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Using `bases`:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The argument `bases` can optionally accept a single instance of `TypeDispatch` or a collection (i.e. a tuple or list) of `TypeDispatch` objects. This can provide functionality similar to multiple inheritance. \n", - "\n", - "These are searched for matching functions if no match in your list of functions:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(str,object) -> f_str\n", - "(bool,object) -> f_bll\n", - "(int,object) -> f_ni2\n", - "(Integral,object) -> f_nin\n", - "(Number,object) -> f_num\n", - "(list,object) -> f_bll\n", - "(object,object) -> NoneType" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def f_str(x:str): return x+'1'\n", - "\n", - "t = TypeDispatch([f_nin,f_ni2,f_num,f_bll,None])\n", - "t2 = TypeDispatch(f_str, bases=t) # you can optionally supply a list of TypeDispatch objects for `bases`.\n", - "t2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(t2[int], f_ni2) # searches `t` b/c not found in `t2`\n", - "test_eq(t2[np.int32], f_nin) # searches `t` b/c not found in `t2`\n", - "test_eq(t2[float], f_num) # searches `t` b/c not found in `t2`\n", - "test_eq(t2[bool], f_bll) # searches `t` b/c not found in `t2`\n", - "test_eq(t2[str], f_str) # found in `t`!\n", - "test_eq(t2('a'), 'a1') # found in `t`!, and uses __call__\n", - "\n", - "o = np.int32(1)\n", - "test_eq(t2(o), 2) # found in `t2` and uses __call__" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Up To Two Arguments" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`TypeDispatch` supports up to two arguments when searching for the appropriate function. The following functions `f1` and `f2` both have two parameters:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(int,float) -> f2\n", - "(Integral,object) -> f1" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def f1(x:numbers.Integral, y): return x+1 #Integral is a numeric type\n", - "def f2(x:int, y:float): return x+y\n", - "t = TypeDispatch([f1,f2])\n", - "t" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - " You can lookup functions from a `TypeDispatch` instance with two parameters like this:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(t[np.int32], f1)\n", - "test_eq(t[int,float], f2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Keep in mind that anything beyond the first two parameters are ignored, and any collisions will be resolved in favor of the most recent function added. In the below example, `f1` is ignored in favor of `f2` because the first two parameters have identical type hints:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(str,int) -> f2" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def f1(a:str, b:int, c:list): return a\n", - "def f2(a: str, b:int): return b\n", - "t = TypeDispatch([f1,f2])\n", - "test_eq(t[str, int], f2)\n", - "t" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Matching" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`Type Dispatch` matches types with functions according to whether the supplied class is a subclass or the same class of the type annotation(s) of associated functions. \n", - "\n", - "Let's consider an example where we try to retrieve the function corresponding to types of `[np.int32, float]`.\n", - "\n", - "In this scenario, `f2` will not be matched. This is because the first type annotation of `f2`, `int`, is not a superclass (or the same class) of `np.int32`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f1(x:numbers.Integral, y): return x+1\n", - "def f2(x:int, y:float): return x+y\n", - "t = TypeDispatch([f1,f2])\n", - "\n", - "assert not issubclass(np.int32, int)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Instead, `f1` is a valid match, as its first argument is annoted with the type `numbers.Integeral`, which `np.int32` is a subclass of: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "assert issubclass(np.int32, numbers.Integral)\n", - "test_eq(t[np.int32,float], f1) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In `f1` , the 2nd parameter `y` is not annotated, which means `TypeDispatch` will match anything where the first argument matches `int` that is not matched with anything else:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "assert issubclass(int, numbers.Integral) # int is a subclass of numbers.Integral\n", - "test_eq(t[int], f1)\n", - "test_eq(t[int,int], f1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If no match is possible, `None` is returned:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(t[float,float], None)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "

TypeDispatch.__call__[source]

\n", - "\n", - "> TypeDispatch.__call__(**\\*`args`**, **\\*\\*`kwargs`**)\n", - "\n", - "Call self as a function." - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "show_doc(TypeDispatch.__call__)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`TypeDispatch` is also callable. When you call an instance of `TypeDispatch`, it will execute the relevant function:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f_arr(x:np.ndarray): return x.sum()\n", - "def f_int(x:np.int32): return x+1\n", - "t = TypeDispatch([f_arr, f_int])\n", - "\n", - "arr = np.array([5,4,3,2,1])\n", - "test_eq(t(arr), 15) # dispatches to f_arr\n", - "\n", - "o = np.int32(1)\n", - "test_eq(t(o), 2) # dispatches to f_int\n", - "assert t.first() is not None " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can also call an instance of of `TypeDispatch` when there are two parameters:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f1(x:numbers.Integral, y): return x+1\n", - "def f2(x:int, y:float): return x+y\n", - "t = TypeDispatch([f1,f2])\n", - "\n", - "test_eq(t(3,2.0), 5)\n", - "test_eq(t(3,2), 4)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When no match is found, a `TypeDispatch` instance becomes an identity function. This default behavior is leveraged by fasatai for data transformations to provide a sensible default when a matching function cannot be found." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(t('a'), 'a')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "

TypeDispatch.returns[source]

\n", - "\n", - "> TypeDispatch.returns(**`x`**)\n", - "\n", - "Get the return type of annotation of `x`." - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "show_doc(TypeDispatch.returns)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can optionally pass an object to `TypeDispatch.returns` and get the return type annotation back:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f1(x:int) -> np.ndarray: return np.array(x)\n", - "def f2(x:str) -> float: return List\n", - "def f3(x:float): return List # f3 has no return type annotation\n", - "\n", - "t = TypeDispatch([f1, f2, f3])\n", - "\n", - "test_eq(t.returns(1), np.ndarray) # dispatched to f1\n", - "test_eq(t.returns('Hello'), float) # dispatched to f2\n", - "test_eq(t.returns(1.0), None) # dispatched to f3\n", - "\n", - "class _Test: pass\n", - "_test = _Test()\n", - "test_eq(t.returns(_test), None) # type `_Test` not found, so None returned" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Using TypeDispatch With Methods" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can use `TypeDispatch` when defining methods as well:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def m_nin(self, x:str|numbers.Integral): return str(x)+'1'\n", - "def m_bll(self, x:bool): self.foo='a'\n", - "def m_num(self, x:numbers.Number): return x*2\n", - "\n", - "t = TypeDispatch([m_nin,m_num,m_bll])\n", - "class A: f = t # set class attribute `f` equal to a TypeDispatch instance\n", - " \n", - "a = A()\n", - "test_eq(a.f(1), '11') #dispatch to m_nin\n", - "test_eq(a.f(1.), 2.) #dispatch to m_num\n", - "test_is(a.f.inst, a)\n", - "\n", - "a.f(False) # this triggers t.m_bll to run, which sets self.foo to 'a'\n", - "test_eq(a.foo, 'a')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As discussed in `TypeDispatch.__call__`, when there is not a match, `TypeDispatch.__call__` becomes an identity function. In the below example, a tuple does not match any type annotations so a tuple is returned:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(a.f(()), ()) " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We extend the previous example by using `bases` to add an additional method that supports tuples:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def m_tup(self, x:tuple): return x+(1,)\n", - "t2 = TypeDispatch(m_tup, bases=t)\n", - "\n", - "class A2: f = t2\n", - "a2 = A2()\n", - "test_eq(a2.f(1), '11')\n", - "test_eq(a2.f(1.), 2.)\n", - "test_is(a2.f.inst, a2)\n", - "a2.f(False)\n", - "test_eq(a2.foo, 'a')\n", - "test_eq(a2.f(()), (1,))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Using TypeDispatch With Class Methods" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can use `TypeDispatch` when defining class methods too:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def m_nin(cls, x:str|numbers.Integral): return str(x)+'1'\n", - "def m_bll(cls, x:bool): cls.foo='a'\n", - "def m_num(cls, x:numbers.Number): return x*2\n", - "\n", - "t = TypeDispatch([m_nin,m_num,m_bll])\n", - "class A: f = t # set class attribute `f` equal to a TypeDispatch\n", - "\n", - "test_eq(A.f(1), '11') #dispatch to m_nin\n", - "test_eq(A.f(1.), 2.) #dispatch to m_num\n", - "test_is(A.f.owner, A)\n", - "\n", - "A.f(False) # this triggers t.m_bll to run, which sets A.foo to 'a'\n", - "test_eq(A.foo, 'a')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## typedispatch Decorator" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class DispatchReg:\n", - " \"A global registry for `TypeDispatch` objects keyed by function name\"\n", - " def __init__(self): self.d = defaultdict(TypeDispatch)\n", - " def __call__(self, f):\n", - " if isinstance(f, (classmethod, staticmethod)): nm = f'{f.__func__.__qualname__}'\n", - " else: nm = f'{f.__qualname__}'\n", - " if isinstance(f, classmethod): f=f.__func__\n", - " self.d[nm].add(f)\n", - " return self.d[nm]\n", - "\n", - "typedispatch = DispatchReg()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@typedispatch\n", - "def f_td_test(x, y): return f'{x}{y}'\n", - "@typedispatch\n", - "def f_td_test(x:numbers.Integral|int, y): return x+1\n", - "@typedispatch\n", - "def f_td_test(x:int, y:float): return x+y\n", - "@typedispatch\n", - "def f_td_test(x:int, y:int): return x*y\n", - "\n", - "test_eq(f_td_test(3,2.0), 5)\n", - "assert issubclass(int, numbers.Integral)\n", - "test_eq(f_td_test(3,2), 6)\n", - "\n", - "test_eq(f_td_test('a','b'), 'ab')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Using typedispatch With other decorators" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can use `typedispatch` with `classmethod` and `staticmethod` decorator" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class A:\n", - " @typedispatch\n", - " def f_td_test(self, x:numbers.Integral, y): return x+1\n", - " @typedispatch\n", - " @classmethod\n", - " def f_td_test(cls, x:int, y:float): return x+y\n", - " @typedispatch\n", - " @staticmethod\n", - " def f_td_test(x:int, y:int): return x*y\n", - " \n", - "test_eq(A.f_td_test(3,2), 6)\n", - "test_eq(A.f_td_test(3,2.0), 5)\n", - "test_eq(A().f_td_test(3,'2.0'), 4)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Casting" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now that we can dispatch on types, let's make it easier to cast objects to a different type." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "_all_=['cast']" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def retain_meta(x, res, as_copy=False):\n", - " \"Call `res.set_meta(x)`, if it exists\"\n", - " if hasattr(res,'set_meta'): res.set_meta(x, as_copy=as_copy)\n", - " return res" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def default_set_meta(self, x, as_copy=False):\n", - " \"Copy over `_meta` from `x` to `res`, if it's missing\"\n", - " if hasattr(x, '_meta') and not hasattr(self, '_meta'):\n", - " meta = x._meta\n", - " if as_copy: meta = copy(meta)\n", - " self._meta = meta\n", - " return self" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "@typedispatch\n", - "def cast(x, typ):\n", - " \"cast `x` to type `typ` (may also change `x` inplace)\"\n", - " res = typ._before_cast(x) if hasattr(typ, '_before_cast') else x\n", - " if risinstance('ndarray', res): res = res.view(typ)\n", - " elif hasattr(res, 'as_subclass'): res = res.as_subclass(typ)\n", - " else:\n", - " try: res.__class__ = typ\n", - " except: res = typ(res)\n", - " return retain_meta(x, res)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This works both for plain python classes:..." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "mk_class('_T1', 'a') # mk_class is a fastai utility that constructs a class.\n", - "class _T2(_T1): pass\n", - "\n", - "t = _T1(a=1)\n", - "t2 = cast(t, _T2) \n", - "assert t2 is t # t2 refers to the same object as t\n", - "assert isinstance(t, _T2) # t also changed in-place\n", - "assert isinstance(t2, _T2)\n", - "\n", - "test_eq_type(_T2(a=1), t2) \n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "...as well as for arrays and tensors." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class _T1(ndarray): pass\n", - "\n", - "t = array([1])\n", - "t2 = cast(t, _T1)\n", - "test_eq(array([1]), t2)\n", - "test_eq(_T1, type(t2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To customize casting for other types, define a separate `cast` function with `typedispatch` for your type." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def retain_type(new, old=None, typ=None, as_copy=False):\n", - " \"Cast `new` to type of `old` or `typ` if it's a superclass\"\n", - " # e.g. old is TensorImage, new is Tensor - if not subclass then do nothing\n", - " if new is None: return\n", - " assert old is not None or typ is not None\n", - " if typ is None:\n", - " if not isinstance(old, type(new)): return new\n", - " typ = old if isinstance(old,type) else type(old)\n", - " # Do nothing the new type is already an instance of requested type (i.e. same type)\n", - " if typ==NoneType or isinstance(new, typ): return new\n", - " return retain_meta(old, cast(new, typ), as_copy=as_copy)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class _T(tuple): pass\n", - "a = _T((1,2))\n", - "b = tuple((1,2))\n", - "c = retain_type(b, typ=_T)\n", - "test_eq_type(c, a)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If `old` has a `_meta` attribute, its content is passed when casting `new` to the type of `old`. In the below example, only the attribute `a`, but not `other_attr` is kept, because `other_attr` is not in `_meta`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class _A():\n", - " set_meta = default_set_meta\n", - " def __init__(self, t): self.t=t\n", - "\n", - "class _B1(_A):\n", - " def __init__(self, t, a=1):\n", - " super().__init__(t)\n", - " self._meta = {'a':a}\n", - " self.other_attr = 'Hello' # will not be kept after casting.\n", - " \n", - "x = _B1(1, a=2)\n", - "b = _A(1)\n", - "c = retain_type(b, old=x)\n", - "test_eq(c._meta, {'a': 2})\n", - "assert not getattr(c, 'other_attr', None)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def retain_types(new, old=None, typs=None):\n", - " \"Cast each item of `new` to type of matching item in `old` if it's a superclass\"\n", - " if not is_listy(new): return retain_type(new, old, typs)\n", - " if typs is not None:\n", - " if isinstance(typs, dict):\n", - " t = first(typs.keys())\n", - " typs = typs[t]\n", - " else: t,typs = typs,None\n", - " else: t = type(old) if old is not None and isinstance(old,type(new)) else type(new)\n", - " return t(L(new, old, typs).map_zip(retain_types, cycled=True))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class T(tuple): pass\n", - "\n", - "t1,t2 = retain_types((1,(1,(1,1))), (2,T((2,T((3,4))))))\n", - "test_eq_type(t1, 1)\n", - "test_eq_type(t2, T((1,T((1,1)))))\n", - "\n", - "t1,t2 = retain_types((1,(1,(1,1))), typs = {tuple: [int, {T: [int, {T: [int,int]}]}]})\n", - "test_eq_type(t1, 1)\n", - "test_eq_type(t2, T((1,T((1,1)))))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def explode_types(o):\n", - " \"Return the type of `o`, potentially in nested dictionaries for thing that are listy\"\n", - " if not is_listy(o): return type(o)\n", - " return {type(o): [explode_types(o_) for o_ in o]}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(explode_types((2,T((2,T((3,4)))))), {tuple: [int, {T: [int, {T: [int,int]}]}]})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Export -" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Converted 00_test.ipynb.\n", - "Converted 01_basics.ipynb.\n", - "Converted 02_foundation.ipynb.\n", - "Converted 03_xtras.ipynb.\n", - "Converted 03a_parallel.ipynb.\n", - "Converted 03b_net.ipynb.\n", - "Converted 04_dispatch.ipynb.\n", - "Converted 05_transform.ipynb.\n", - "Converted 06_docments.ipynb.\n", - "Converted 07_meta.ipynb.\n", - "Converted 08_script.ipynb.\n", - "Converted index.ipynb.\n", - "Converted parallel_win.ipynb.\n" - ] - } - ], - "source": [ - "#|hide\n", - "#|eval: false\n", - "from nbdev import nbdev_export\n", - "nbdev_export()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "jupytext": { - "split_at_heading": true - }, - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/nbs/05_transform.ipynb b/nbs/05_transform.ipynb deleted file mode 100644 index 8967ed36..00000000 --- a/nbs/05_transform.ipynb +++ /dev/null @@ -1,2003 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|default_exp transform" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "from fastcore.imports import *\n", - "from fastcore.foundation import *\n", - "from fastcore.utils import *\n", - "from fastcore.dispatch import *\n", - "import inspect" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from __future__ import annotations\n", - "from nbdev.showdoc import *\n", - "from fastcore.test import *\n", - "from fastcore.nb_imports import *" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Transforms\n", - "\n", - "> Definition of `Transform` and `Pipeline`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The classes here provide functionality for creating a composition of *partially reversible functions*. By \"partially reversible\" we mean that a transform can be `decode`d, creating a form suitable for display. This is not necessarily identical to the original form (e.g. a transform that changes a byte tensor to a float tensor does not recreate a byte tensor when decoded, since that may lose precision, and a float tensor can be displayed already).\n", - "\n", - "Classes are also provided and for composing transforms, and mapping them over collections. `Pipeline` is a transform which composes several `Transform`, knowing how to decode them or show an encoded item." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Transform -" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "_tfm_methods = 'encodes','decodes','setups'\n", - "\n", - "def _is_tfm_method(n, f): return n in _tfm_methods and callable(f)\n", - "\n", - "class _TfmDict(dict):\n", - " def __setitem__(self, k, v):\n", - " if not _is_tfm_method(k, v): return super().__setitem__(k,v)\n", - " if k not in self: super().__setitem__(k,TypeDispatch())\n", - " self[k].add(v)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class _TfmMeta(type):\n", - " def __new__(cls, name, bases, dict):\n", - " res = super().__new__(cls, name, bases, dict)\n", - " for nm in _tfm_methods:\n", - " base_td = [getattr(b,nm,None) for b in bases]\n", - " if nm in res.__dict__: getattr(res,nm).bases = base_td\n", - " else: setattr(res, nm, TypeDispatch(bases=base_td))\n", - " # _TfmMeta.__call__ shadows the signature of inheriting classes, set it back\n", - " res.__signature__ = inspect.signature(res.__init__)\n", - " return res\n", - "\n", - " def __call__(cls, *args, **kwargs):\n", - " f = first(args)\n", - " n = getattr(f, '__name__', None)\n", - " if _is_tfm_method(n, f):\n", - " getattr(cls,n).add(f)\n", - " return f\n", - " obj = super().__call__(*args, **kwargs)\n", - " # _TfmMeta.__new__ replaces cls.__signature__ which breaks the signature of a callable\n", - " # instances of cls, fix it\n", - " if hasattr(obj, '__call__'): obj.__signature__ = inspect.signature(obj.__call__)\n", - " return obj\n", - "\n", - " @classmethod\n", - " def __prepare__(cls, name, bases): return _TfmDict()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def _get_name(o):\n", - " if hasattr(o,'__qualname__'): return o.__qualname__\n", - " if hasattr(o,'__name__'): return o.__name__\n", - " return o.__class__.__name__" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def _is_tuple(o): return isinstance(o, tuple) and not hasattr(o, '_fields')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class Transform(metaclass=_TfmMeta):\n", - " \"Delegates (`__call__`,`decode`,`setup`) to (encodes,decodes,setups) if `split_idx` matches\"\n", - " split_idx,init_enc,order,train_setup = None,None,0,None\n", - " def __init__(self, enc=None, dec=None, split_idx=None, order=None):\n", - " self.split_idx = ifnone(split_idx, self.split_idx)\n", - " if order is not None: self.order=order\n", - " self.init_enc = enc or dec\n", - " if not self.init_enc: return\n", - "\n", - " self.encodes,self.decodes,self.setups = TypeDispatch(),TypeDispatch(),TypeDispatch()\n", - " if enc:\n", - " self.encodes.add(enc)\n", - " self.order = getattr(enc,'order',self.order)\n", - " if len(type_hints(enc)) > 0: self.input_types = union2tuple(first(type_hints(enc).values()))\n", - " self._name = _get_name(enc)\n", - " if dec: self.decodes.add(dec)\n", - "\n", - " @property\n", - " def name(self): return getattr(self, '_name', _get_name(self))\n", - " def __call__(self, x, **kwargs): return self._call('encodes', x, **kwargs)\n", - " def decode (self, x, **kwargs): return self._call('decodes', x, **kwargs)\n", - " def __repr__(self): return f'{self.name}:\\nencodes: {self.encodes}decodes: {self.decodes}'\n", - "\n", - " def setup(self, items=None, train_setup=False):\n", - " train_setup = train_setup if self.train_setup is None else self.train_setup\n", - " return self.setups(getattr(items, 'train', items) if train_setup else items)\n", - "\n", - " def _call(self, fn, x, split_idx=None, **kwargs):\n", - " if split_idx!=self.split_idx and self.split_idx is not None: return x\n", - " return self._do_call(getattr(self, fn), x, **kwargs)\n", - "\n", - " def _do_call(self, f, x, **kwargs):\n", - " if not _is_tuple(x):\n", - " if f is None: return x\n", - " ret = f.returns(x) if hasattr(f,'returns') else None\n", - " return retain_type(f(x, **kwargs), x, ret)\n", - " res = tuple(self._do_call(f, x_, **kwargs) for x_ in x)\n", - " return retain_type(res, x)\n", - "\n", - "add_docs(Transform, decode=\"Delegate to decodes to undo transform\", setup=\"Delegate to setups to set up transform\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "### Transform\n", - "\n", - "> Transform (enc=None, dec=None, split_idx=None, order=None)\n", - "\n", - "Delegates (`__call__`,`decode`,`setup`) to (encodes,decodes,setups) if `split_idx` matches" - ], - "text/plain": [ - "" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "show_doc(Transform)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A `Transform` is the main building block of the fastai data pipelines. In the most general terms a transform can be any function you want to apply to your data, however the `Transform` class provides several mechanisms that make the process of building them easy and flexible." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### The main `Transform` features:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "- **Type dispatch** - Type annotations are used to determine if a transform should be applied to the given argument. It also gives an option to provide several implementations and it choses the one to run based on the type. This is useful for example when running both independent and dependent variables through the pipeline where some transforms only make sense for one and not the other. Another usecase is designing a transform that handles different data formats. Note that if a transform takes multiple arguments only the type of the first one is used for dispatch. \n", - "- **Handling of tuples** - When a tuple (or a subclass of tuple) of data is passed to a transform it will get applied to each element separately. You can opt out of this behavior by passing a list or an `L`, as only tuples gets this specific behavior. An alternative is to use `ItemTransform` defined below, which will always take the input as a whole.\n", - "- **Reversability** - A transform can be made reversible by implementing the decodes method. This is mainly used to turn something like a category which is encoded as a number back into a label understandable by humans for showing purposes. Like the regular call method, the `decode` method that is used to decode will be applied over each element of a tuple separately.\n", - "- **Type propagation** - Whenever possible a transform tries to return data of the same type it received. Mainly used to maintain semantics of things like `ArrayImage` which is a thin wrapper of pytorch's `Tensor`. You can opt out of this behavior by adding `->None` return type annotation.\n", - "- **Preprocessing** - The `setup` method can be used to perform any one-time calculations to be later used by the transform, for example generating a vocabulary to encode categorical data.\n", - "- **Filtering based on the dataset type** - By setting the `split_idx` flag you can make the transform be used only in a specific `DataSource` subset like in training, but not validation.\n", - "- **Ordering** - You can set the `order` attribute which the `Pipeline` uses when it needs to merge two lists of transforms.\n", - "- **Appending new behavior with decorators** - You can easily extend an existing `Transform` by creating encodes or decodes methods for new data types. You can put those new methods outside the original transform definition and decorate them with the class you wish them patched into. This can be used by the fastai library users to add their own behavior, or multiple modules contributing to the same transform." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Defining a `Transform`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are a few ways to create a transform with different ratios of simplicity to flexibility.\n", - "- **Extending the `Transform` class** - Use inheritence to implement the methods you want.\n", - "- **Passing methods to the constructor** - Instantiate the `Transform` class and pass your functions as `enc` and `dec` arguments.\n", - "- **@Transform decorator** - Turn any function into a `Transform` by just adding a decorator - very straightforward if all you need is a single encodes implementation.\n", - "- **Passing a function to fastai APIs** - Same as above, but when passing a function to other transform aware classes like `Pipeline` or `TfmdDS` you don't even need a decorator. Your function will get converted to a `Transform` automatically." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A simple way to create a `Transform` is to pass a function to the constructor. In the below example, we pass an anonymous function that does integer division by 2:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "f = Transform(lambda o:o//2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you call this transform, it will apply the transformation:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq_type(f(2), 1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Another way to define a Transform is to extend the `Transform` class:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class A(Transform): pass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, to enable your transform to do something, you have to define an encodes method. Note that we can use the class name as a decorator to add this method to the original class." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@A\n", - "def encodes(self, x): return x+1\n", - "\n", - "f1 = A()\n", - "test_eq(f1(1), 2) # f1(1) is the same as f1.encode(1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In addition to adding an encodes method, we can also add a decodes method. This enables you to call the `decode` method (without an s). For more information about the purpose of decodes, see the discussion about Reversibility in [the above section](#The-main-Transform-features)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Just like with encodes, you can add a decodes method to the original class by using the class name as a decorator:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class B(A): pass\n", - "\n", - "@B\n", - "def decodes(self, x): return x-1\n", - "\n", - "f2 = B()\n", - "test_eq(f2.decode(2), 1)\n", - "\n", - "test_eq(f2(1), 2) # uses A's encode method from the parent class" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you do not define an encodes or decodes method the original value will be returned:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class _Tst(Transform): pass \n", - "\n", - "f3 = _Tst() # no encodes or decodes method have been defined\n", - "test_eq_type(f3.decode(2.0), 2.0)\n", - "test_eq_type(f3(2), 2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Transforms can be created from class methods too:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class A:\n", - " @classmethod\n", - " def create(cls, x:int): return x+1\n", - "test_eq(Transform(A.create)(1), 2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "# Test extension of a tfm method defined in the class\n", - "class A(Transform):\n", - " def encodes(self, x): return 'obj'\n", - "\n", - "@A\n", - "def encodes(self, x:int): return 'int'\n", - "\n", - "a = A()\n", - "test_eq(a.encodes(0), 'int')\n", - "test_eq(a.encodes(0.0), 'obj')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Defining Transforms With A Decorator" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`Transform` can be used as a decorator to turn a function into a `Transform`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@Transform\n", - "def f(x): return x//2\n", - "test_eq_type(f(2), 1)\n", - "test_eq_type(f.decode(2.0), 2.0)\n", - "\n", - "@Transform\n", - "def f(x): return x*2\n", - "test_eq_type(f(2), 4)\n", - "test_eq_type(f.decode(2.0), 2.0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Typed Dispatch and Transforms" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also apply different transformations depending on the type of the input passed by using `TypedDispatch`. `TypedDispatch` automatically works with `Transform` when using type hints:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class A(Transform): pass\n", - "\n", - "@A\n", - "def encodes(self, x:int): return x//2\n", - "\n", - "@A\n", - "def encodes(self, x:float): return x+1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When we pass in an `int`, this calls the first encodes method:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "f = A()\n", - "test_eq_type(f(3), 1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When we pass in a `float`, this calls the second encodes method:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq_type(f(2.), 3.)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When we pass in a type that is not specified in encodes, the original value is returned:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(f('a'), 'a')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If the type annotation is a tuple, then any type in the tuple will match:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class MyClass(int): pass\n", - "\n", - "class A(Transform):\n", - " def encodes(self, x:MyClass|float): return x/2\n", - " def encodes(self, x:str|list): return str(x)+'_1'\n", - "\n", - "f = A()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The below two examples match the first encodes, with a type of `MyClass` and `float`, respectively:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(f(MyClass(2)), 1.) # input is of type MyClass \n", - "test_eq(f(6.0), 3.0) # input is of type float" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next two examples match the second `encodes` method, with a type of `str` and `list`, respectively:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(f('a'), 'a_1') # input is of type str\n", - "test_eq(f(['a','b','c']), \"['a', 'b', 'c']_1\") # input is of type list" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Casting Types With Transform" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Without any intervention it is easy for operations to change types in Python. For example, `FloatSubclass` (defined below) becomes a `float` after performing multiplication:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class FloatSubclass(float): pass\n", - "test_eq_type(FloatSubclass(3.0) * 2, 6.0)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This behavior is often not desirable when performing transformations on data. Therefore, `Transform` will attempt to cast the output to be of the same type as the input by default. In the below example, the output will be cast to a `FloatSubclass` type to match the type of the input:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@Transform\n", - "def f(x): return x*2\n", - "\n", - "test_eq_type(f(FloatSubclass(3.0)), FloatSubclass(6.0))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can optionally turn off casting by annotating the transform function with a return type of `None`: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@Transform\n", - "def f(x)-> None: return x*2 # Same transform as above, but with a -> None annotation\n", - "\n", - "test_eq_type(f(FloatSubclass(3.0)), 6.0) # Casting is turned off because of -> None annotation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, `Transform` will only cast output back to the input type when the input is a subclass of the output. In the below example, the input is of type `FloatSubclass` which is not a subclass of the output which is of type `str`. Therefore, the output doesn't get cast back to `FloatSubclass` and stays as type `str`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@Transform\n", - "def f(x): return str(x)\n", - " \n", - "test_eq_type(f(Float(2.)), '2.0')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Just like encodes, the decodes method will cast outputs to match the input type in the same way. In the below example, the output of decodes remains of type `MySubclass`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class MySubclass(int): pass\n", - "\n", - "def enc(x): return MySubclass(x+1)\n", - "def dec(x): return x-1\n", - "\n", - "\n", - "f = Transform(enc,dec)\n", - "t = f(1) # t is of type MySubclass\n", - "test_eq_type(f.decode(t), MySubclass(1)) # the output of decode is cast to MySubclass to match the input type." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Apply Transforms On Subsets With `split_idx`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can apply transformations to subsets of data by specifying a `split_idx` property. If a transform has a `split_idx` then it's only applied if the `split_idx` param matches. In the below example, we set `split_idx` equal to `1`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def enc(x): return x+1\n", - "def dec(x): return x-1\n", - "f = Transform(enc,dec)\n", - "f.split_idx = 1" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The transformations are applied when a matching `split_idx` parameter is passed:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(f(1, split_idx=1),2)\n", - "test_eq(f.decode(2, split_idx=1),1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "On the other hand, transformations are ignored when the `split_idx` parameter does not match:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(f(1, split_idx=0), 1)\n", - "test_eq(f.decode(2, split_idx=0), 2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Transforms on Lists" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Transform operates on lists as a whole, **not element-wise**:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class A(Transform):\n", - " def encodes(self, x): return dict(x)\n", - " def decodes(self, x): return list(x.items())\n", - " \n", - "f = A()\n", - "_inp = [(1,2), (3,4)]\n", - "t = f(_inp)\n", - "\n", - "test_eq(t, dict(_inp))\n", - "test_eq(f.decodes(t), _inp)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "f.split_idx = 1\n", - "test_eq(f(_inp, split_idx=1), dict(_inp))\n", - "test_eq(f(_inp, split_idx=0), _inp)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you want a transform to operate on a list elementwise, you must implement this appropriately in the encodes and decodes methods:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class AL(Transform): pass\n", - "\n", - "@AL\n", - "def encodes(self, x): return [x_+1 for x_ in x]\n", - "\n", - "@AL\n", - "def decodes(self, x): return [x_-1 for x_ in x]\n", - "\n", - "f = AL()\n", - "t = f([1,2])\n", - "\n", - "test_eq(t, [2,3])\n", - "test_eq(f.decode(t), [1,2])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Transforms on Tuples\n", - "\n", - "Unlike lists, `Transform` operates on tuples element-wise." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def neg_int(x): return -x\n", - "f = Transform(neg_int)\n", - "\n", - "test_eq(f((1,2,3)), (-1,-2,-3))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Transforms will also apply `TypedDispatch` element-wise on tuples when an input type annotation is specified. In the below example, the values `1.0` and `3.0` are ignored because they are of type `float`, not `int`: " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def neg_int(x:int): return -x\n", - "f = Transform(neg_int)\n", - "\n", - "test_eq(f((1.0, 2, 3.0)), (1.0, -2, 3.0))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "test_eq(f((1,)), (-1,))\n", - "test_eq(f((1.,)), (1.,))\n", - "test_eq(f.decode((1,2)), (1,2))\n", - "test_eq(f.input_types, int)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Another example of how `Transform` can use `TypedDispatch` with tuples is shown below:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class B(Transform): pass\n", - "\n", - "@B\n", - "def encodes(self, x:int): return x+1\n", - "\n", - "@B\n", - "def encodes(self, x:str): return x+'hello'\n", - "\n", - "@B\n", - "def encodes(self, x): return str(x)+'!'" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If the input is not an `int` or `str`, the third `encodes` method will apply:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "b = B()\n", - "test_eq(b([1]), '[1]!') \n", - "test_eq(b([1.0]), '[1.0]!')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, if the input is a tuple, then the appropriate method will apply according to the type of each element in the tuple:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(b(('1',)), ('1hello',))\n", - "test_eq(b((1,2)), (2,3))\n", - "test_eq(b(('a',1.0)), ('ahello','1.0!'))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "@B\n", - "def decodes(self, x:int): return x-1\n", - "\n", - "test_eq(b.decode((2,)), (1,))\n", - "test_eq(b.decode(('2',)), ('2',))\n", - "assert pickle.loads(pickle.dumps(b))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Dispatching over tuples works recursively, by the way:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class B(Transform):\n", - " def encodes(self, x:int): return x+1\n", - " def encodes(self, x:str): return x+'_hello'\n", - " def decodes(self, x:int): return x-1\n", - " def decodes(self, x:str): return x.replace('_hello', '')\n", - "\n", - "f = B()\n", - "start = (1.,(2,'3'))\n", - "t = f(start)\n", - "test_eq_type(t, (1.,(3,'3_hello')))\n", - "test_eq(f.decode(t), start)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Dispatching also works with `typing` module type classes, like `numbers.integral`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@Transform\n", - "def f(x:numbers.Integral): return x+1\n", - "\n", - "t = f((1,'1',1))\n", - "test_eq(t, (2, '1', 2))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class InplaceTransform(Transform):\n", - " \"A `Transform` that modifies in-place and just returns whatever it's passed\"\n", - " def _call(self, fn, x, split_idx=None, **kwargs):\n", - " super()._call(fn,x,split_idx,**kwargs)\n", - " return x" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "import pandas as pd" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class A(InplaceTransform): pass\n", - "\n", - "@A\n", - "def encodes(self, x:pd.Series): x.fillna(10, inplace=True)\n", - " \n", - "f = A()\n", - "\n", - "test_eq_type(f(pd.Series([1,2,None])),pd.Series([1,2,10],dtype=np.float64)) #fillna fills with floats." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class DisplayedTransform(Transform):\n", - " \"A transform with a `__repr__` that shows its attrs\"\n", - "\n", - " @property\n", - " def name(self): return f\"{super().name} -- {getattr(self,'__stored_args__',{})}\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Transforms normally are represented by just their class name and a list of encodes and decodes implementations:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "A:\n", - "encodes: (object,object) -> noop\n", - "decodes: (object,object) -> noop" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "class A(Transform): encodes,decodes = noop,noop\n", - "f = A()\n", - "f" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "A `DisplayedTransform` will in addition show the contents of all attributes listed in the comma-delimited string `self.store_attrs`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "A -- {'a': 1, 'b': 2}:\n", - "encodes: (object,object) -> noop\n", - "decodes: " - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "class A(DisplayedTransform):\n", - " encodes = noop\n", - " def __init__(self, a, b=2):\n", - " super().__init__()\n", - " store_attr()\n", - " \n", - "A(a=1,b=2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### ItemTransform -" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class ItemTransform(Transform):\n", - " \"A transform that always take tuples as items\"\n", - " _retain = True\n", - " def __call__(self, x, **kwargs): return self._call1(x, '__call__', **kwargs)\n", - " def decode(self, x, **kwargs): return self._call1(x, 'decode', **kwargs)\n", - " def _call1(self, x, name, **kwargs):\n", - " if not _is_tuple(x): return getattr(super(), name)(x, **kwargs)\n", - " y = getattr(super(), name)(list(x), **kwargs)\n", - " if not self._retain: return y\n", - " if is_listy(y) and not isinstance(y, tuple): y = tuple(y)\n", - " return retain_type(y, x)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`ItemTransform` is the class to use to opt out of the default behavior of `Transform`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class AIT(ItemTransform): \n", - " def encodes(self, xy): x,y=xy; return (x+y,y)\n", - " def decodes(self, xy): x,y=xy; return (x-y,y)\n", - " \n", - "f = AIT()\n", - "test_eq(f((1,2)), (3,2))\n", - "test_eq(f.decode((3,2)), (1,2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you pass a special tuple subclass, the usual retain type behavior of `Transform` will keep it:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class _T(tuple): pass\n", - "x = _T((1,2))\n", - "test_eq_type(f(x), _T((3,2)))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "f.split_idx = 0\n", - "test_eq_type(f((1,2)), (1,2))\n", - "test_eq_type(f((1,2), split_idx=0), (3,2))\n", - "test_eq_type(f.decode((1,2)), (1,2))\n", - "test_eq_type(f.decode((3,2), split_idx=0), (1,2))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "class Get(ItemTransform):\n", - " _retain = False\n", - " def encodes(self, x): return x[0]\n", - " \n", - "g = Get()\n", - "test_eq(g([1,2,3]), 1)\n", - "test_eq(g(L(1,2,3)), 1)\n", - "test_eq(g(np.array([1,2,3])), 1)\n", - "test_eq_type(g((['a'], ['b', 'c'])), ['a'])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "class A(ItemTransform): \n", - " def encodes(self, x): return _T((x,x))\n", - " def decodes(self, x): return _T(x)\n", - " \n", - "f = A()\n", - "test_eq(type(f.decode((1,1))), _T)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Func -" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def get_func(t, name, *args, **kwargs):\n", - " \"Get the `t.name` (potentially partial-ized with `args` and `kwargs`) or `noop` if not defined\"\n", - " f = nested_callable(t, name)\n", - " return f if not (args or kwargs) else partial(f, *args, **kwargs)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This works for any kind of `t` supporting `getattr`, so a class or a module." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(get_func(operator, 'neg', 2)(), -2)\n", - "test_eq(get_func(operator.neg, '__call__')(2), -2)\n", - "test_eq(get_func(list, 'foobar')([2]), [2])\n", - "a = [2,1]\n", - "get_func(list, 'sort')(a)\n", - "test_eq(a, [1,2])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Transforms are built with multiple-dispatch: a given function can have several methods depending on the type of the object received. This is done directly with the `TypeDispatch` module and type-annotation in `Transform`, but you can also use the following class." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class Func():\n", - " \"Basic wrapper around a `name` with `args` and `kwargs` to call on a given type\"\n", - " def __init__(self, name, *args, **kwargs): self.name,self.args,self.kwargs = name,args,kwargs\n", - " def __repr__(self): return f'sig: {self.name}({self.args}, {self.kwargs})'\n", - " def _get(self, t): return get_func(t, self.name, *self.args, **self.kwargs)\n", - " def __call__(self,t): return mapped(self._get, t)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can call the `Func` object on any module name or type, even a list of types. It will return the corresponding function (with a default to `noop` if nothing is found) or list of functions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(Func('sqrt')(math), math.sqrt)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class _Sig():\n", - " def __getattr__(self,k):\n", - " def _inner(*args, **kwargs): return Func(k, *args, **kwargs)\n", - " return _inner\n", - "\n", - "Sig = _Sig()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "### Sig\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "show_doc(Sig, name=\"Sig\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`Sig` is just sugar-syntax to create a `Func` object more easily with the syntax `Sig.name(*args, **kwargs)`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "f = Sig.sqrt()\n", - "test_eq(f(math), math.sqrt)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Pipeline -" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def compose_tfms(x, tfms, is_enc=True, reverse=False, **kwargs):\n", - " \"Apply all `func_nm` attribute of `tfms` on `x`, maybe in `reverse` order\"\n", - " if reverse: tfms = reversed(tfms)\n", - " for f in tfms:\n", - " if not is_enc: f = f.decode\n", - " x = f(x, **kwargs)\n", - " return x" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def to_int (x): return Int(x)\n", - "def to_float(x): return Float(x)\n", - "def double (x): return x*2\n", - "def half(x)->None: return x/2" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def test_compose(a, b, *fs): test_eq_type(compose_tfms(a, tfms=map(Transform,fs)), b)\n", - "\n", - "test_compose(1, Int(1), to_int)\n", - "test_compose(1, Float(1), to_int,to_float)\n", - "test_compose(1, Float(2), to_int,to_float,double)\n", - "test_compose(2.0, 2.0, to_int,double,half)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class A(Transform):\n", - " def encodes(self, x:float): return Float(x+1)\n", - " def decodes(self, x): return x-1\n", - " \n", - "tfms = [A(), Transform(math.sqrt)]\n", - "t = compose_tfms(3., tfms=tfms)\n", - "test_eq_type(t, Float(2.))\n", - "test_eq(compose_tfms(t, tfms=tfms, is_enc=False), 1.)\n", - "test_eq(compose_tfms(4., tfms=tfms, reverse=True), 3.)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "tfms = [A(), Transform(math.sqrt)]\n", - "test_eq(compose_tfms((9,3.), tfms=tfms), (3,2.))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def mk_transform(f):\n", - " \"Convert function `f` to `Transform` if it isn't already one\"\n", - " f = instantiate(f)\n", - " return f if isinstance(f,(Transform,Pipeline)) else Transform(f)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def gather_attrs(o, k, nm):\n", - " \"Used in __getattr__ to collect all attrs `k` from `self.{nm}`\"\n", - " if k.startswith('_') or k==nm: raise AttributeError(k)\n", - " att = getattr(o,nm)\n", - " res = [t for t in att.attrgot(k) if t is not None]\n", - " if not res: raise AttributeError(k)\n", - " return res[0] if len(res)==1 else L(res)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "def gather_attr_names(o, nm):\n", - " \"Used in __dir__ to collect all attrs `k` from `self.{nm}`\"\n", - " return L(getattr(o,nm)).map(dir).concat().unique()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|export\n", - "class Pipeline:\n", - " \"A pipeline of composed (for encode/decode) transforms, setup with types\"\n", - " def __init__(self, funcs=None, split_idx=None):\n", - " self.split_idx,self.default = split_idx,None\n", - " if funcs is None: funcs = []\n", - " if isinstance(funcs, Pipeline): self.fs = funcs.fs\n", - " else:\n", - " if isinstance(funcs, Transform): funcs = [funcs]\n", - " self.fs = L(ifnone(funcs,[noop])).map(mk_transform).sorted(key='order')\n", - " for f in self.fs:\n", - " name = camel2snake(type(f).__name__)\n", - " a = getattr(self,name,None)\n", - " if a is not None: f = L(a)+f\n", - " setattr(self, name, f)\n", - "\n", - " def setup(self, items=None, train_setup=False):\n", - " tfms = self.fs[:]\n", - " self.fs.clear()\n", - " for t in tfms: self.add(t,items, train_setup)\n", - "\n", - " def add(self,ts, items=None, train_setup=False):\n", - " if not is_listy(ts): ts=[ts]\n", - " for t in ts: t.setup(items, train_setup)\n", - " self.fs+=ts\n", - " self.fs = self.fs.sorted(key='order')\n", - "\n", - " def __call__(self, o): return compose_tfms(o, tfms=self.fs, split_idx=self.split_idx)\n", - " def __repr__(self): return f\"Pipeline: {' -> '.join([f.name for f in self.fs if f.name != 'noop'])}\"\n", - " def __getitem__(self,i): return self.fs[i]\n", - " def __setstate__(self,data): self.__dict__.update(data)\n", - " def __getattr__(self,k): return gather_attrs(self, k, 'fs')\n", - " def __dir__(self): return super().__dir__() + gather_attr_names(self, 'fs')\n", - "\n", - " def decode (self, o, full=True):\n", - " if full: return compose_tfms(o, tfms=self.fs, is_enc=False, reverse=True, split_idx=self.split_idx)\n", - " #Not full means we decode up to the point the item knows how to show itself.\n", - " for f in reversed(self.fs):\n", - " if self._is_showable(o): return o\n", - " o = f.decode(o, split_idx=self.split_idx)\n", - " return o\n", - "\n", - " def show(self, o, ctx=None, **kwargs):\n", - " o = self.decode(o, full=False)\n", - " o1 = (o,) if not _is_tuple(o) else o\n", - " if hasattr(o, 'show'): ctx = o.show(ctx=ctx, **kwargs)\n", - " else:\n", - " for o_ in o1:\n", - " if hasattr(o_, 'show'): ctx = o_.show(ctx=ctx, **kwargs)\n", - " return ctx\n", - "\n", - " def _is_showable(self, o):\n", - " if hasattr(o, 'show'): return True\n", - " if _is_tuple(o): return all(hasattr(o_, 'show') for o_ in o)\n", - " return False" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "add_docs(Pipeline,\n", - " __call__=\"Compose `__call__` of all `fs` on `o`\",\n", - " decode=\"Compose `decode` of all `fs` on `o`\",\n", - " show=\"Show `o`, a single item from a tuple, decoding as needed\",\n", - " add=\"Add transforms `ts`\",\n", - " setup=\"Call each tfm's `setup` in order\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`Pipeline` is a wrapper for `compose_tfms`. You can pass instances of `Transform` or regular functions in `funcs`, the `Pipeline` will wrap them all in `Transform` (and instantiate them if needed) during the initialization. It handles the transform `setup` by adding them one at a time and calling setup on each, goes through them in order in `__call__` or `decode` and can `show` an object by applying decoding the transforms up until the point it gets an object that knows how to show itself." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Empty pipeline is noop\n", - "pipe = Pipeline()\n", - "test_eq(pipe(1), 1)\n", - "test_eq(pipe((1,)), (1,))\n", - "# Check pickle works\n", - "assert pickle.loads(pickle.dumps(pipe))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class IntFloatTfm(Transform):\n", - " def encodes(self, x): return Int(x)\n", - " def decodes(self, x): return Float(x)\n", - " foo=1\n", - "\n", - "int_tfm=IntFloatTfm()\n", - "\n", - "def neg(x): return -x\n", - "neg_tfm = Transform(neg, neg)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pipe = Pipeline([neg_tfm, int_tfm])\n", - "\n", - "start = 2.0\n", - "t = pipe(start)\n", - "test_eq_type(t, Int(-2))\n", - "test_eq_type(pipe.decode(t), Float(start))\n", - "test_stdout(lambda:pipe.show(t), '-2')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pipe = Pipeline([neg_tfm, int_tfm])\n", - "t = pipe(start)\n", - "test_stdout(lambda:pipe.show(pipe((1.,2.))), '-1\\n-2')\n", - "test_eq(pipe.foo, 1)\n", - "assert 'foo' in dir(pipe)\n", - "assert 'int_float_tfm' in dir(pipe)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can add a single transform or multiple transforms `ts` using `Pipeline.add`. Transforms will be ordered by `Transform.order`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pipe = Pipeline([neg_tfm, int_tfm])\n", - "class SqrtTfm(Transform):\n", - " order=-1\n", - " def encodes(self, x): \n", - " return x**(.5)\n", - " def decodes(self, x): return x**2\n", - "pipe.add(SqrtTfm())\n", - "test_eq(pipe(4),-2)\n", - "test_eq(pipe.decode(-2),4)\n", - "pipe.add([SqrtTfm(),SqrtTfm()])\n", - "test_eq(pipe(256),-2)\n", - "test_eq(pipe.decode(-2),256)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Transforms are available as attributes named with the snake_case version of the names of their types. Attributes in transforms can be directly accessed as attributes of the pipeline." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(pipe.int_float_tfm, int_tfm)\n", - "test_eq(pipe.foo, 1)\n", - "\n", - "pipe = Pipeline([int_tfm, int_tfm])\n", - "pipe.int_float_tfm\n", - "test_eq(pipe.int_float_tfm[0], int_tfm)\n", - "test_eq(pipe.foo, [1,1])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Check opposite order\n", - "pipe = Pipeline([int_tfm,neg_tfm])\n", - "t = pipe(start)\n", - "test_eq(t, -2)\n", - "test_stdout(lambda:pipe.show(t), '-2')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class A(Transform):\n", - " def encodes(self, x): return int(x)\n", - " def decodes(self, x): return Float(x)\n", - "\n", - "pipe = Pipeline([neg_tfm, A])\n", - "t = pipe(start)\n", - "test_eq_type(t, -2)\n", - "test_eq_type(pipe.decode(t), Float(start))\n", - "test_stdout(lambda:pipe.show(t), '-2.0')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "s2 = (1,2)\n", - "pipe = Pipeline([neg_tfm, A])\n", - "t = pipe(s2)\n", - "test_eq_type(t, (-1,-2))\n", - "test_eq_type(pipe.decode(t), (Float(1.),Float(2.)))\n", - "test_stdout(lambda:pipe.show(t), '-1.0\\n-2.0')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from PIL import Image" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class ArrayImage(ndarray):\n", - " _show_args = {'cmap':'viridis'}\n", - " def __new__(cls, x, *args, **kwargs):\n", - " if isinstance(x,tuple): super().__new__(cls, x, *args, **kwargs)\n", - " if args or kwargs: raise RuntimeError('Unknown array init args')\n", - " if not isinstance(x,ndarray): x = array(x)\n", - " return x.view(cls)\n", - " \n", - " def show(self, ctx=None, figsize=None, **kwargs):\n", - " if ctx is None: _,ctx = plt.subplots(figsize=figsize)\n", - " ctx.imshow(im, **{**self._show_args, **kwargs})\n", - " ctx.axis('off')\n", - " return ctx\n", - " \n", - "im = Image.open(TEST_IMAGE)\n", - "im_t = ArrayImage(im)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f1(x:ArrayImage): return -x\n", - "def f2(x): return Image.open(x).resize((128,128))\n", - "def f3(x:Image.Image): return(ArrayImage(array(x)))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pipe = Pipeline([f2,f3,f1])\n", - "t = pipe(TEST_IMAGE)\n", - "test_eq(type(t), ArrayImage)\n", - "test_eq(t, -array(f3(f2(TEST_IMAGE))))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "pipe = Pipeline([f2,f3])\n", - "t = pipe(TEST_IMAGE)\n", - "ax = pipe.show(t)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#test_fig_exists(ax)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#Check filtering is properly applied\n", - "add1 = B()\n", - "add1.split_idx = 1\n", - "pipe = Pipeline([neg_tfm, A(), add1])\n", - "test_eq(pipe(start), -2)\n", - "pipe.split_idx=1\n", - "test_eq(pipe(start), -1)\n", - "pipe.split_idx=0\n", - "test_eq(pipe(start), -2)\n", - "for t in [None, 0, 1]:\n", - " pipe.split_idx=t\n", - " test_eq(pipe.decode(pipe(start)), start)\n", - " test_stdout(lambda: pipe.show(pipe(start)), \"-2.0\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def neg(x): return -x\n", - "test_eq(type(mk_transform(neg)), Transform)\n", - "test_eq(type(mk_transform(math.sqrt)), Transform)\n", - "test_eq(type(mk_transform(lambda a:a*2)), Transform)\n", - "test_eq(type(mk_transform(Pipeline([neg]))), Pipeline)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Methods" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#TODO: method examples" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "#### Pipeline.__call__\n", - "\n", - "> Pipeline.__call__ (o)\n", - "\n", - "Compose `__call__` of all `fs` on `o`" - ], - "text/plain": [ - "" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "show_doc(Pipeline.__call__)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "#### Pipeline.decode\n", - "\n", - "> Pipeline.decode (o, full=True)\n", - "\n", - "Compose `decode` of all `fs` on `o`" - ], - "text/plain": [ - "" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "show_doc(Pipeline.decode)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "#### Pipeline.setup\n", - "\n", - "> Pipeline.setup (items=None, train_setup=False)\n", - "\n", - "Call each tfm's `setup` in order" - ], - "text/plain": [ - "" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "show_doc(Pipeline.setup)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "During the setup, the `Pipeline` starts with no transform and adds them one at a time, so that during its setup, each transform gets the items processed up to its point and not after." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "#Test is with TfmdList" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Export -" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#|hide\n", - "#|eval: false\n", - "from nbdev import nbdev_export\n", - "nbdev_export()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "jupytext": { - "split_at_heading": true - }, - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} From 9e6e7c71207bf8d0cabd66352a77d1f9a31a3fc6 Mon Sep 17 00:00:00 2001 From: Rens Date: Mon, 3 Mar 2025 14:21:47 +0100 Subject: [PATCH 050/182] rename nbs to keep sequential numbering after removing 4 and 5 --- fastcore/docments.py | 36 +++++++------- fastcore/meta.py | 36 +++++++------- fastcore/py2pyi.py | 52 ++++++++++---------- fastcore/script.py | 24 ++++----- fastcore/style.py | 20 ++++---- fastcore/xdg.py | 22 ++++----- fastcore/xml.py | 40 +++++++-------- nbs/{06_docments.ipynb => 04_docments.ipynb} | 0 nbs/{07_meta.ipynb => 05_meta.ipynb} | 0 nbs/{08_script.ipynb => 06_script.ipynb} | 0 nbs/{09_xdg.ipynb => 07_xdg.ipynb} | 0 nbs/{10_style.ipynb => 08_style.ipynb} | 0 nbs/{11_xml.ipynb => 09_xml.ipynb} | 0 nbs/{12_py2pyi.ipynb => 10_py2pyi.ipynb} | 0 nbs/{13_external.ipynb => 11_external.ipynb} | 0 15 files changed, 115 insertions(+), 115 deletions(-) rename nbs/{06_docments.ipynb => 04_docments.ipynb} (100%) rename nbs/{07_meta.ipynb => 05_meta.ipynb} (100%) rename nbs/{08_script.ipynb => 06_script.ipynb} (100%) rename nbs/{09_xdg.ipynb => 07_xdg.ipynb} (100%) rename nbs/{10_style.ipynb => 08_style.ipynb} (100%) rename nbs/{11_xml.ipynb => 09_xml.ipynb} (100%) rename nbs/{12_py2pyi.ipynb => 10_py2pyi.ipynb} (100%) rename nbs/{13_external.ipynb => 11_external.ipynb} (100%) diff --git a/fastcore/docments.py b/fastcore/docments.py index f68a5b3d..3c0ce412 100644 --- a/fastcore/docments.py +++ b/fastcore/docments.py @@ -1,8 +1,8 @@ """Document parameters using comments.""" -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_docments.ipynb. +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/04_docments.ipynb. -# %% ../nbs/06_docments.ipynb 2 +# %% ../nbs/04_docments.ipynb 2 from __future__ import annotations import re,ast @@ -22,7 +22,7 @@ __all__ = ['empty', 'docstring', 'parse_docstring', 'isdataclass', 'get_dataclass_source', 'get_source', 'get_name', 'qual_name', 'docments', 'extract_docstrings'] -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def docstring(sym): "Get docstring for `sym` for functions ad classes" if isinstance(sym, str): return sym @@ -30,27 +30,27 @@ def docstring(sym): if not res and isclass(sym): res = getdoc(sym.__init__) return res or "" -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def parse_docstring(sym): "Parse a numpy-style docstring in `sym`" return AttrDict(**docscrape.NumpyDocString(docstring(sym))) -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def isdataclass(s): "Check if `s` is a dataclass but not a dataclass' instance" return is_dataclass(s) and isclass(s) -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def get_dataclass_source(s): "Get source code for dataclass `s`" return getsource(s) if not getattr(s, "__module__") == '__main__' else "" -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def get_source(s): "Get source code for string, function object or dataclass `s`" return getsource(s) if isfunction(s) or ismethod(s) else get_dataclass_source(s) if isdataclass(s) else s -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def _parses(s): "Parse Python code in string, function object or dataclass `s`" return parse(dedent(get_source(s))) @@ -85,10 +85,10 @@ def _param_locs(s, returns=True, args_kwargs=False): return res return None -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb empty = Parameter.empty -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def _get_comment(line, arg, comments, parms): if line in comments: return comments[line].strip() line -= 1 @@ -105,7 +105,7 @@ def _get_full(p, docs): elif p.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD): anno = p.kind return AttrDict(docment=docs.get(p.name), anno=anno, default=p.default) -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def _merge_doc(dm, npdoc): if not npdoc: return dm if not dm.anno or dm.anno==empty: dm.anno = npdoc.type @@ -118,14 +118,14 @@ def _merge_docs(dms, npdocs): if 'return' in dms: params['return'] = _merge_doc(dms['return'], npdocs['Returns']) return params -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def _get_property_name(p): "Get the name of property `p`" if hasattr(p, 'fget'): return p.fget.func.__qualname__ if hasattr(p.fget, 'func') else p.fget.__qualname__ else: return next(iter(re.findall(r'\'(.*)\'', str(p)))).split('.')[-1] -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def get_name(obj): "Get the name of `obj`" if hasattr(obj, '__name__'): return obj.__name__ @@ -134,14 +134,14 @@ def get_name(obj): elif type(obj)==property: return _get_property_name(obj) else: return str(obj).split('.')[-1] -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def qual_name(obj): "Get the qualified name of `obj`" if hasattr(obj,'__qualname__'): return obj.__qualname__ if ismethod(obj): return f"{get_name(obj.__self__)}.{get_name(fn)}" return get_name(obj) -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def _docments(s, returns=True, eval_str=False, args_kwargs=False): "`dict` of parameter names to 'docment-style' comments in function or string `s`" nps = parse_docstring(s) @@ -160,7 +160,7 @@ def _docments(s, returns=True, eval_str=False, args_kwargs=False): if k in hints: v['anno'] = hints.get(k) return res -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb @delegates(_docments) def docments(elt, full=False, args_kwargs=False, **kwargs): "Generates a `docment`" @@ -178,7 +178,7 @@ def _update_docments(f, r): if not full: r = {k:v['docment'] for k,v in r.items()} return AttrDict(r) -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def _get_params(node): params = [a.arg for a in node.args.args] if node.args.vararg: params.append(f"*{node.args.vararg.arg}") @@ -215,7 +215,7 @@ def visit_Module(self, node): if module_doc: self.docstrings['_module'] = (module_doc, "") self.generic_visit(node) -# %% ../nbs/06_docments.ipynb +# %% ../nbs/04_docments.ipynb def extract_docstrings(code): "Create a dict from function/class/method names to tuples of docstrings and param lists" extractor = _DocstringExtractor() diff --git a/fastcore/meta.py b/fastcore/meta.py index bd338632..5543f751 100644 --- a/fastcore/meta.py +++ b/fastcore/meta.py @@ -1,30 +1,30 @@ """Metaclasses""" -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/07_meta.ipynb. +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/05_meta.ipynb. # %% auto 0 __all__ = ['test_sig', 'FixSigMeta', 'PrePostInitMeta', 'AutoInit', 'NewChkMeta', 'BypassNewMeta', 'empty2none', 'anno_dict', 'use_kwargs_dict', 'use_kwargs', 'delegates', 'method', 'funcs_kwargs'] -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb from .imports import * from .test import * from contextlib import contextmanager from copy import copy import inspect -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def test_sig(f, b): "Test the signature of an object" test_eq(str(inspect.signature(f)), b) -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def _rm_self(sig): sigd = dict(sig.parameters) sigd.pop('self') return sig.replace(parameters=sigd.values()) -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb class FixSigMeta(type): "A metaclass that fixes the signature on classes that override `__new__`" def __new__(cls, name, bases, dict): @@ -32,7 +32,7 @@ def __new__(cls, name, bases, dict): if res.__init__ is not object.__init__: res.__signature__ = _rm_self(inspect.signature(res.__init__)) return res -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb class PrePostInitMeta(FixSigMeta): "A metaclass that calls optional `__pre_init__` and `__post_init__` methods" def __call__(cls, *args, **kwargs): @@ -43,12 +43,12 @@ def __call__(cls, *args, **kwargs): if hasattr(res,'__post_init__'): res.__post_init__(*args,**kwargs) return res -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb class AutoInit(metaclass=PrePostInitMeta): "Same as `object`, but no need for subclasses to call `super().__init__`" def __pre_init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb class NewChkMeta(FixSigMeta): "Metaclass to avoid recreating object passed to constructor" def __call__(cls, x=None, *args, **kwargs): @@ -56,7 +56,7 @@ def __call__(cls, x=None, *args, **kwargs): res = super().__call__(*((x,) + args), **kwargs) return res -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb class BypassNewMeta(FixSigMeta): "Metaclass: casts `x` to this class if it's of type `cls._bypass_type`" def __call__(cls, x=None, *args, **kwargs): @@ -66,20 +66,20 @@ def __call__(cls, x=None, *args, **kwargs): if cls!=x.__class__: x.__class__ = cls return x -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def empty2none(p): "Replace `Parameter.empty` with `None`" return None if p==inspect.Parameter.empty else p -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def anno_dict(f): "`__annotation__ dictionary with `empty` cast to `None`, returning empty if doesn't exist" return {k:empty2none(v) for k,v in getattr(f, '__annotations__', {}).items()} -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def _mk_param(n,d=None): return inspect.Parameter(n, inspect.Parameter.KEYWORD_ONLY, default=d) -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def use_kwargs_dict(keep=False, **kwargs): "Decorator: replace `**kwargs` in signature with `names` params" def _f(f): @@ -93,7 +93,7 @@ def _f(f): return f return _f -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def use_kwargs(names, keep=False): "Decorator: replace `**kwargs` in signature with `names` params" def _f(f): @@ -107,7 +107,7 @@ def _f(f): return f return _f -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def delegates(to:FunctionType=None, # Delegatee keep=False, # Keep `kwargs` in decorated function? but:list=None, # Exclude these parameters from signature @@ -135,13 +135,13 @@ def _f(f): return f return _f -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def method(f): "Mark `f` as a method" # `1` is a dummy instance since Py3 doesn't allow `None` any more return MethodType(f, 1) -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def _funcs_kwargs(cls, as_method): old_init = cls.__init__ def _init(self, *args, **kwargs): @@ -157,7 +157,7 @@ def _init(self, *args, **kwargs): if hasattr(cls, '__signature__'): cls.__signature__ = _rm_self(inspect.signature(cls.__init__)) return cls -# %% ../nbs/07_meta.ipynb +# %% ../nbs/05_meta.ipynb def funcs_kwargs(as_method=False): "Replace methods in `cls._methods` with those from `kwargs`" if callable(as_method): return _funcs_kwargs(as_method, False) diff --git a/fastcore/py2pyi.py b/fastcore/py2pyi.py index 7bd7e343..6c50f2e5 100644 --- a/fastcore/py2pyi.py +++ b/fastcore/py2pyi.py @@ -1,9 +1,9 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/12_py2pyi.ipynb. +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/10_py2pyi.ipynb. # %% auto 0 __all__ = ['functypes', 'imp_mod', 'has_deco', 'sig2str', 'ast_args', 'create_pyi', 'py2pyi', 'replace_wildcards'] -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb import ast, sys, inspect, re, os, importlib.util, importlib.machinery from ast import parse, unparse @@ -11,7 +11,7 @@ from .utils import * from .meta import delegates -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def imp_mod(module_path, package=None): "Import dynamically the module referenced in `fn`" module_path = str(module_path) @@ -24,11 +24,11 @@ def imp_mod(module_path, package=None): spec.loader.exec_module(module) return module -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _get_tree(mod): return parse(getsource(mod)) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb @patch def __repr__(self:ast.AST): return unparse(self) @@ -39,10 +39,10 @@ def _repr_markdown_(self:ast.AST): {self!r} ```""" -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb functypes = (ast.FunctionDef,ast.AsyncFunctionDef) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _deco_id(d:Union[ast.Name,ast.Attribute])->bool: "Get the id for AST node `d`" return d.id if isinstance(d, ast.Name) else d.func.id @@ -51,7 +51,7 @@ def has_deco(node:Union[ast.FunctionDef,ast.AsyncFunctionDef], name:str)->bool: "Check if a function node `node` has a decorator named `name`" return any(_deco_id(d)==name for d in getattr(node, 'decorator_list', [])) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _get_proc(node): if isinstance(node, ast.ClassDef): return _proc_class if not isinstance(node, functypes): return None @@ -59,20 +59,20 @@ def _get_proc(node): if has_deco(node, 'patch'): return _proc_patched return _proc_func -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _proc_tree(tree, mod): for node in tree.body: proc = _get_proc(node) if proc: proc(node, mod) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _clean_patched_node(node): "Clean the patched node in-place." # When moving a patched node to its parent, we no longer need the patch decorator and parent annotation. node.decorator_list = [deco for deco in node.decorator_list if getattr(deco, "id", None) != "patch"] node.args.args[0].annotation = None -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _is_empty_class(node): if not isinstance(node, ast.ClassDef): return False if len(node.body) != 1: return False @@ -81,7 +81,7 @@ def _is_empty_class(node): if isinstance(child, ast.Expr) and isinstance(child.value, (ast.Ellipsis, ast.Str)): return True return False -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _add_patched_node_to_parent(node, parent): "Add a patched node to its parent." # if the patch node updates an existing class method, let's replace it. @@ -93,7 +93,7 @@ def _add_patched_node_to_parent(node, parent): if _is_empty_class(parent): parent.body = [node] else: parent.body.append(node) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _proc_patches(tree, mod): "Move all patched methods to their parents." class_nodes = {} # {class_name: position in node tree} @@ -121,31 +121,31 @@ def _proc_patches(tree, mod): i -= 1 # as we've removed the patched node from the tree we need to decrement the loop counter i += 1 -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _proc_mod(mod): tree = _get_tree(mod) _proc_tree(tree, mod) _proc_patches(tree, mod) return tree -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def sig2str(sig): s = str(sig) s = re.sub(r"", r'\1', s) s = re.sub(r"dynamic_module\.", "", s) return s -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def ast_args(func): sig = signature(func) return ast.parse(f"def _{sig2str(sig)}: ...").body[0].args -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _body_ellip(n: ast.AST): stidx = 1 if isinstance(n.body[0], ast.Expr) and isinstance(n.body[0].value, ast.Str) else 0 n.body[stidx:] = [ast.Expr(ast.Constant(...))] -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _update_func(node, sym): """Replace the parameter list of the source code of a function `f` with a different signature. Replace the body of the function with just `pass`, and remove any decorators named 'delegates'""" @@ -162,15 +162,15 @@ def _update_func(node, sym): _body_ellip(node) node.decorator_list = [d for d in node.decorator_list if _deco_id(d) != 'delegates'] -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _proc_body(node, mod): _body_ellip(node) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _proc_func(node, mod): sym = getattr(mod, node.name) _update_func(node, sym) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _proc_patched(node, mod): ann = node.args.args[0].annotation if hasattr(ann, 'elts'): ann = ann.elts[0] @@ -178,12 +178,12 @@ def _proc_patched(node, mod): sym = getattr(cls, node.name) _update_func(node, sym) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def _proc_class(node, mod): cls = getattr(mod, node.name) _proc_tree(node, cls) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb def create_pyi(fn, package=None): "Convert `fname.py` to `fname.pyi` by removing function bodies and expanding `delegates` kwargs" fn = Path(fn) @@ -192,10 +192,10 @@ def create_pyi(fn, package=None): res = unparse(tree) fn.with_suffix('.pyi').write_text(res) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb from .script import call_parse -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb @call_parse def py2pyi(fname:str, # The file name to convert package:str=None # The parent package @@ -203,7 +203,7 @@ def py2pyi(fname:str, # The file name to convert "Convert `fname.py` to `fname.pyi` by removing function bodies and expanding `delegates` kwargs" create_pyi(fname, package) -# %% ../nbs/12_py2pyi.ipynb +# %% ../nbs/10_py2pyi.ipynb @call_parse def replace_wildcards( # Path to the Python file to process diff --git a/fastcore/script.py b/fastcore/script.py index 23d81f34..34695951 100644 --- a/fastcore/script.py +++ b/fastcore/script.py @@ -1,41 +1,41 @@ """A fast way to turn your python function into a script.""" -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/08_script.ipynb. +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/06_script.ipynb. # %% auto 0 __all__ = ['SCRIPT_INFO', 'store_true', 'store_false', 'bool_arg', 'clean_type_str', 'Param', 'anno_parser', 'args_from_prog', 'call_parse'] -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb import inspect,argparse,shutil from functools import wraps,partial from .imports import * from .utils import * from .docments import docments -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb def store_true(): "Placeholder to pass to `Param` for `store_true` action" pass -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb def store_false(): "Placeholder to pass to `Param` for `store_false` action" pass -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb def bool_arg(v): "Use as `type` for `Param` to get `bool` behavior" return str2bool(v) -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb def clean_type_str(x:str): x = str(x) x = re.sub(r"(enum |class|function|__main__\.|\ at.*)", '', x) x = re.sub(r"(<|>|'|\ )", '', x) # spl characters return x -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb class Param: "A parameter in a function used in `anno_parser` or `call_parse`" def __init__(self, help="", type=None, opt=True, action=None, nargs=None, const=None, @@ -68,14 +68,14 @@ def __repr__(self): if self.help and self.type is None: return f"<{self.help}>" if self.help and self.type is not None: return f"{clean_type_str(self.type)} <{self.help}>" -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb class _HelpFormatter(argparse.HelpFormatter): def __init__(self, prog, indent_increment=2): cols = shutil.get_terminal_size((120,30))[0] super().__init__(prog, max_help_position=cols//2, width=cols, indent_increment=indent_increment) def _expand_help(self, action): return self._get_help_string(action) -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb def anno_parser(func, # Function to get arguments from prog:str=None): # The name of the program "Look at params (annotated with `Param`) in func and return an `ArgumentParser`" @@ -89,7 +89,7 @@ def anno_parser(func, # Function to get arguments from p.add_argument(f"--xtra", help=argparse.SUPPRESS, type=str) return p -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb def args_from_prog(func, prog): "Extract args from `prog`" if prog is None or '#' not in prog: return {} @@ -102,10 +102,10 @@ def args_from_prog(func, prog): if t: args[k] = t(v) return args -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb SCRIPT_INFO = SimpleNamespace(func=None) -# %% ../nbs/08_script.ipynb +# %% ../nbs/06_script.ipynb def call_parse(func=None, nested=False): "Decorator to create a simple CLI from `func` using `anno_parser`" if func is None: return partial(call_parse, nested=nested) diff --git a/fastcore/style.py b/fastcore/style.py index a3c970c1..ca11effe 100644 --- a/fastcore/style.py +++ b/fastcore/style.py @@ -1,31 +1,31 @@ """Fast styling for friendly CLIs.""" -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/10_style.ipynb. +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/08_style.ipynb. # %% auto 0 __all__ = ['style_codes', 'S', 'StyleCode', 'Style', 'demo'] -# %% ../nbs/10_style.ipynb +# %% ../nbs/08_style.ipynb # 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 = 'bold dim italic underline blink invert hidden strikethrough' -# %% ../nbs/10_style.ipynb +# %% ../nbs/08_style.ipynb 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 +# %% ../nbs/08_style.ipynb 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 +# %% ../nbs/08_style.ipynb # 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), @@ -36,13 +36,13 @@ def _mk_codes(s, start, typ, fmt=None, **kwargs): **_mk_codes(_fmt, 21, 'reset', 'reset_{}', reset=0, reset_bold=22)} style_codes = {k:v for k,v in style_codes.items() if '' not in k} -# %% ../nbs/10_style.ipynb +# %% ../nbs/08_style.ipynb 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 +# %% ../nbs/08_style.ipynb class Style: "A minimal terminal text styler." def __init__(self, codes=None): self.codes = [] if codes is None else codes @@ -60,15 +60,15 @@ def __repr__(self): res += ' '.join(o.name for o in self.codes) if self.codes else 'none' return res+'>' -# %% ../nbs/10_style.ipynb +# %% ../nbs/08_style.ipynb S = Style() -# %% ../nbs/10_style.ipynb +# %% ../nbs/08_style.ipynb def _demo(name, code): s = getattr(S,name) print(s(f'{code.code:>3} {name:16}')) -# %% ../nbs/10_style.ipynb +# %% ../nbs/08_style.ipynb def demo(): "Demonstrate all available styles and their codes." for k,v in style_codes.items(): _demo(k,v) diff --git a/fastcore/xdg.py b/fastcore/xdg.py index 32d02751..27db706d 100644 --- a/fastcore/xdg.py +++ b/fastcore/xdg.py @@ -1,21 +1,21 @@ """XDG Base Directory Specification helpers.""" -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/09_xdg.ipynb. +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/07_xdg.ipynb. # %% auto 0 __all__ = ['xdg_cache_home', 'xdg_config_dirs', 'xdg_config_home', 'xdg_data_dirs', 'xdg_data_home', 'xdg_runtime_dir', 'xdg_state_home'] -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb from .utils import * -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def _path_from_env(variable, default): value = os.environ.get(variable) if value and os.path.isabs(value): return Path(value) return default -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def _paths_from_env(variable, default): value = os.environ.get(variable) if value: @@ -23,38 +23,38 @@ def _paths_from_env(variable, default): if paths: return paths return default -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def xdg_cache_home(): "Path corresponding to `XDG_CACHE_HOME`" return _path_from_env("XDG_CACHE_HOME", Path.home()/".cache") -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def xdg_config_dirs(): "Paths corresponding to `XDG_CONFIG_DIRS`" return _paths_from_env("XDG_CONFIG_DIRS", [Path("/etc/xdg")]) -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def xdg_config_home(): "Path corresponding to `XDG_CONFIG_HOME`" return _path_from_env("XDG_CONFIG_HOME", Path.home()/".config") -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def xdg_data_dirs(): "Paths corresponding to XDG_DATA_DIRS`" return _paths_from_env( "XDG_DATA_DIRS", [Path(o) for o in "/usr/local/share/:/usr/share/".split(":")]) -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def xdg_data_home(): "Path corresponding to `XDG_DATA_HOME`" return _path_from_env("XDG_DATA_HOME", Path.home()/".local"/"share") -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def xdg_runtime_dir(): "Path corresponding to `XDG_RUNTIME_DIR`" value = os.getenv("XDG_RUNTIME_DIR") return Path(value) if value and os.path.isabs(value) else None -# %% ../nbs/09_xdg.ipynb +# %% ../nbs/07_xdg.ipynb def xdg_state_home(): "Path corresponding to `XDG_STATE_HOME`" return _path_from_env("XDG_STATE_HOME", Path.home()/".local"/"state") diff --git a/fastcore/xml.py b/fastcore/xml.py index 1e5cad13..8d19e639 100644 --- a/fastcore/xml.py +++ b/fastcore/xml.py @@ -1,6 +1,6 @@ """Concise generation of XML.""" -# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/11_xml.ipynb. +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/09_xml.ipynb. # %% auto 0 __all__ = ['voids', 'attrmap', 'valmap', 'FT', 'ft', 'Html', 'Safe', 'to_xml', 'highlight', 'showtags', 'Head', 'Title', 'Meta', @@ -12,7 +12,7 @@ 'Object', 'Embed', 'Param', 'Video', 'Audio', 'Source', 'Canvas', 'Svg', 'Math', 'Script', 'Noscript', 'Template', 'Slot'] -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb from .utils import * import types,json @@ -22,10 +22,10 @@ from functools import partial from html import escape -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def _fix_k(k): return k if k=='_' else k.lstrip('_').replace('_', '-') -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb _specials = set('@.-!~:[](){}$%^&*+=|/?<>,`') def attrmap(o): @@ -34,13 +34,13 @@ def attrmap(o): _for='for', fr='for', htmlFor='for').get(o, o) return _fix_k(o) -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def valmap(o): if is_listy(o): return ' '.join(map(str,o)) if o else None if isinstance(o, dict): return '; '.join(f"{k}:{v}" for k,v in o.items()) if o else None return o -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def _flatten_tuple(tup): if not any(isinstance(item, tuple) for item in tup): return tup result = [] @@ -49,13 +49,13 @@ def _flatten_tuple(tup): else: result.append(item) return tuple(result) -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def _preproc(c, kw, attrmap=attrmap, valmap=valmap): if len(c)==1 and isinstance(c[0], (types.GeneratorType, map, filter)): c = tuple(c[0]) attrs = {attrmap(k.lower()):valmap(v) for k,v in kw.items() if v is not None} return _flatten_tuple(c),attrs -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb class FT: "A 'Fast Tag' structure, containing `tag`,`children`,and `attrs`" def __init__(self, tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs): @@ -104,12 +104,12 @@ def set(self, *c, **kw): self.attrs = {**self.attrs, **kw} return self.changed() -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def ft(tag:str, *c, void_:bool=False, attrmap:callable=attrmap, valmap:callable=valmap, ft_cls=FT, **kw): "Create an `FT` structure for `to_xml()`" return ft_cls(tag.lower(),*_preproc(c,kw,attrmap=attrmap, valmap=valmap), void_=void_) -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb voids = set('area base br col command embed hr img input keygen link meta param source track wbr !doctype'.split()) _g = globals() _all_ = ['Head', 'Title', 'Meta', 'Link', 'Style', 'Body', 'Pre', 'Code', @@ -124,22 +124,22 @@ def ft(tag:str, *c, void_:bool=False, attrmap:callable=attrmap, valmap:callable= for o in _all_: _g[o] = partial(ft, o.lower(), void_=o.lower() in voids) -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def Html(*c, doctype=True, **kwargs)->FT: "An HTML tag, optionally preceeded by `!DOCTYPE HTML`" res = ft('html', *c, **kwargs) if not doctype: return res return (ft('!DOCTYPE', html=True, void_=True), res) -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb class Safe(str): def __html__(self): return self -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def _escape(s): return '' if s is None else s.__html__() if hasattr(s, '__html__') else escape(s) if isinstance(s, str) else s def _noescape(s): return '' if s is None else s.__html__() if hasattr(s, '__html__') else s -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def _to_attr(k,v): if isinstance(v,bool): if v==True : return str(k) @@ -153,7 +153,7 @@ def _to_attr(k,v): if "'" in v: v = v.replace("'", "'") return f'{k}={qt}{v}{qt}' -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb _block_tags = {'div', 'p', 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tfoot', 'html', 'head', 'body', 'meta', 'title', '!doctype', 'input', 'script', 'link', 'style', 'tr', 'th', 'td', 'section', 'article', 'nav', 'aside', 'header', @@ -164,7 +164,7 @@ def _to_attr(k,v): def _is_whitespace_significant(elm): return elm.tag in {'pre', 'code', 'textarea', 'script'} or elm.get('contenteditable') == 'true' -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def _to_xml(elm, lvl=0, indent=True, do_escape=True): "Convert `FT` element tree into an XML string" esc_fn = _escape if do_escape else _noescape @@ -203,19 +203,19 @@ def _to_xml(elm, lvl=0, indent=True, do_escape=True): if not is_void: res += f'{sp}{cltag}{nl_end}' return Safe(res) -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def to_xml(elm, lvl=0, indent=True, do_escape=True): "Convert `ft` element tree into an XML string" return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape)) FT.__html__ = to_xml -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def highlight(s, lang='html'): "Markdown to syntax-highlight `s` in language `lang`" return f'```{lang}\n{to_xml(s)}\n```' -# %% ../nbs/11_xml.ipynb +# %% ../nbs/09_xml.ipynb def showtags(s): return f"""
 {escape(to_xml(s))}
@@ -223,7 +223,7 @@ def showtags(s):
 
 FT._repr_markdown_ = highlight
 
-# %% ../nbs/11_xml.ipynb
+# %% ../nbs/09_xml.ipynb
 def __getattr__(tag):
     if tag.startswith('_') or tag[0].islower(): raise AttributeError
     tag = _fix_k(tag)
diff --git a/nbs/06_docments.ipynb b/nbs/04_docments.ipynb
similarity index 100%
rename from nbs/06_docments.ipynb
rename to nbs/04_docments.ipynb
diff --git a/nbs/07_meta.ipynb b/nbs/05_meta.ipynb
similarity index 100%
rename from nbs/07_meta.ipynb
rename to nbs/05_meta.ipynb
diff --git a/nbs/08_script.ipynb b/nbs/06_script.ipynb
similarity index 100%
rename from nbs/08_script.ipynb
rename to nbs/06_script.ipynb
diff --git a/nbs/09_xdg.ipynb b/nbs/07_xdg.ipynb
similarity index 100%
rename from nbs/09_xdg.ipynb
rename to nbs/07_xdg.ipynb
diff --git a/nbs/10_style.ipynb b/nbs/08_style.ipynb
similarity index 100%
rename from nbs/10_style.ipynb
rename to nbs/08_style.ipynb
diff --git a/nbs/11_xml.ipynb b/nbs/09_xml.ipynb
similarity index 100%
rename from nbs/11_xml.ipynb
rename to nbs/09_xml.ipynb
diff --git a/nbs/12_py2pyi.ipynb b/nbs/10_py2pyi.ipynb
similarity index 100%
rename from nbs/12_py2pyi.ipynb
rename to nbs/10_py2pyi.ipynb
diff --git a/nbs/13_external.ipynb b/nbs/11_external.ipynb
similarity index 100%
rename from nbs/13_external.ipynb
rename to nbs/11_external.ipynb

From 9e3b468352d85e1fa0f1bec0d2009fbab419f08f Mon Sep 17 00:00:00 2001
From: Rens 
Date: Mon, 3 Mar 2025 14:28:43 +0100
Subject: [PATCH 051/182] remove multiple dispatch mention from readme.

---
 README.md       | 10 +++++-----
 nbs/index.ipynb |  2 +-
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/README.md b/README.md
index b6472b0d..480429d4 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,11 @@
 Python is a powerful, dynamic language. Rather than bake everything into
 the language, it lets the programmer customize it to make it work for
 them. `fastcore` uses this flexibility to add to Python features
-inspired by other languages we’ve loved, like multiple dispatch from
-Julia, mixins from Ruby, and currying, binding, and more from Haskell.
-It also adds some “missing features” and clean up some rough edges in
-the Python standard library, such as simplifying parallel processing,
-and bringing ideas from NumPy over to Python’s `list` type.
+inspired by other languages we’ve loved, mixins from Ruby, and currying,
+binding, and more from Haskell. It also adds some “missing features” and
+clean up some rough edges in the Python standard library, such as
+simplifying parallel processing, and bringing ideas from NumPy over to
+Python’s `list` type.
 
 ## Getting started
 
diff --git a/nbs/index.ipynb b/nbs/index.ipynb
index cb45cf5f..1917e299 100644
--- a/nbs/index.ipynb
+++ b/nbs/index.ipynb
@@ -25,7 +25,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Python is a powerful, dynamic language. Rather than bake everything into the language, it lets the programmer customize it to make it work for them. `fastcore` uses this flexibility to add to Python features inspired by other languages we've loved, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some \"missing features\" and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python's `list` type."
+    "Python is a powerful, dynamic language. Rather than bake everything into the language, it lets the programmer customize it to make it work for them. `fastcore` uses this flexibility to add to Python features inspired by other languages we've loved, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some \"missing features\" and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python's `list` type."
    ]
   },
   {

From 628ea2c6fbe190def79cccc236e16a23cf7315a6 Mon Sep 17 00:00:00 2001
From: Rens 
Date: Mon, 3 Mar 2025 14:51:52 +0100
Subject: [PATCH 052/182] update get_source_link to be plum compatible

---
 fastcore/xtras.py  |  4 ++--
 nbs/03_xtras.ipynb | 32 ++++++++++++++++----------------
 2 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/fastcore/xtras.py b/fastcore/xtras.py
index 36dea3cd..9d52bffa 100644
--- a/fastcore/xtras.py
+++ b/fastcore/xtras.py
@@ -429,8 +429,8 @@ def exec_eval(code,   # Code to exec/eval
     else: exec(code, g, l)
 
 # %% ../nbs/03_xtras.ipynb
-def _is_type_dispatch(x): return type(x).__name__ == "TypeDispatch"
-def _unwrapped_type_dispatch_func(x): return x.first() if _is_type_dispatch(x) else x
+def _is_type_dispatch(x): return type(x).__name__ == "Function"  # assumes plum-dispatch library is used
+def _unwrapped_type_dispatch_func(x): return x.methods[0].implementation if _is_type_dispatch(x) else x
 
 def _is_property(x): return type(x)==property
 def _has_property_getter(x): return _is_property(x) and hasattr(x, 'fget') and hasattr(x.fget, 'func')
diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb
index f514b0e8..7f91bef0 100644
--- a/nbs/03_xtras.ipynb
+++ b/nbs/03_xtras.ipynb
@@ -1440,7 +1440,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L385){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L389){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ReindexCollection\n",
        "\n",
@@ -1451,7 +1451,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L385){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L389){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ReindexCollection\n",
        "\n",
@@ -1523,7 +1523,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L396){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "###### ReindexCollection.reindex\n",
        "\n",
@@ -1534,7 +1534,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L396){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "###### ReindexCollection.reindex\n",
        "\n",
@@ -1623,7 +1623,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L404){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.cache_clear\n",
        "\n",
@@ -1634,7 +1634,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L404){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.cache_clear\n",
        "\n",
@@ -1688,7 +1688,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L397){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.shuffle\n",
        "\n",
@@ -1699,7 +1699,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L397){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.shuffle\n",
        "\n",
@@ -1896,8 +1896,8 @@
    "outputs": [],
    "source": [
     "#|export\n",
-    "def _is_type_dispatch(x): return type(x).__name__ == \"TypeDispatch\"\n",
-    "def _unwrapped_type_dispatch_func(x): return x.first() if _is_type_dispatch(x) else x\n",
+    "def _is_type_dispatch(x): return type(x).__name__ == \"Function\"  # assumes plum-dispatch library is used\n",
+    "def _unwrapped_type_dispatch_func(x): return x.methods[0].implementation if _is_type_dispatch(x) else x\n",
     "\n",
     "def _is_property(x): return type(x)==property\n",
     "def _has_property_getter(x): return _is_property(x) and hasattr(x, 'fget') and hasattr(x.fget, 'func')\n",
@@ -2252,7 +2252,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L507){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L530){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### EventTimer\n",
        "\n",
@@ -2263,7 +2263,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L507){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L530){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### EventTimer\n",
        "\n",
@@ -2377,7 +2377,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L539){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L562){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PartialFormatter\n",
        "\n",
@@ -2388,7 +2388,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L539){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L562){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PartialFormatter\n",
        "\n",
@@ -2593,7 +2593,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L596){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L619){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ContextManagers\n",
        "\n",
@@ -2604,7 +2604,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/xtras.py#L596){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L619){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ContextManagers\n",
        "\n",

From 8d300d312c029ea0d0312bbcc30f8684980a5c75 Mon Sep 17 00:00:00 2001
From: Rens 
Date: Mon, 3 Mar 2025 14:55:46 +0100
Subject: [PATCH 053/182] remove mentions of dispatch and transform from readme

---
 README.md       | 3 ---
 nbs/index.ipynb | 2 --
 2 files changed, 5 deletions(-)

diff --git a/README.md b/README.md
index 480429d4..f7075961 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,6 @@ with the `-latest` suffix
 - `fastcore.foundation`: Mixins, delegation, composition, and more
 - `fastcore.xtras`: Utility functions to help with functional-style
   programming, parallel processing, and more
-- `fastcore.dispatch`: Multiple dispatch methods
-- `fastcore.transform`: Pipelines of composed partially reversible
-  transformations
 
 To get started, we recommend you read through [the fastcore
 tour](https://fastcore.fast.ai/tour.html).
diff --git a/nbs/index.ipynb b/nbs/index.ipynb
index 1917e299..4166ec75 100644
--- a/nbs/index.ipynb
+++ b/nbs/index.ipynb
@@ -46,8 +46,6 @@
     "- `fastcore.test`: Simple testing functions\n",
     "- `fastcore.foundation`: Mixins, delegation, composition, and more\n",
     "- `fastcore.xtras`: Utility functions to help with functional-style programming, parallel processing, and more\n",
-    "- `fastcore.dispatch`: Multiple dispatch methods\n",
-    "- `fastcore.transform`: Pipelines of composed partially reversible transformations\n",
     "\n",
     "To get started, we recommend you read through [the fastcore tour](https://fastcore.fast.ai/tour.html)."
    ]

From fa33ea8e3cdd58b617000149a1f3e2532a93a471 Mon Sep 17 00:00:00 2001
From: Rens 
Date: Mon, 3 Mar 2025 14:58:36 +0100
Subject: [PATCH 054/182] remove prints containing dispatch and transform nbs

---
 nbs/00_test.ipynb | 20 +-------------------
 1 file changed, 1 insertion(+), 19 deletions(-)

diff --git a/nbs/00_test.ipynb b/nbs/00_test.ipynb
index c2e11d19..0ce004f3 100644
--- a/nbs/00_test.ipynb
+++ b/nbs/00_test.ipynb
@@ -765,25 +765,7 @@
    "cell_type": "code",
    "execution_count": null,
    "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Converted 00_test.ipynb.\n",
-      "Converted 01_basics.ipynb.\n",
-      "Converted 02_foundation.ipynb.\n",
-      "Converted 03_xtras.ipynb.\n",
-      "Converted 03a_parallel.ipynb.\n",
-      "Converted 03b_net.ipynb.\n",
-      "Converted 04_dispatch.ipynb.\n",
-      "Converted 05_transform.ipynb.\n",
-      "Converted 07_meta.ipynb.\n",
-      "Converted 08_script.ipynb.\n",
-      "Converted index.ipynb.\n"
-     ]
-    }
-   ],
+   "outputs": [],
    "source": [
     "#|hide\n",
     "#|eval: false\n",

From cee6fd1ba1b739023075c3fe6a336ab5999be1f8 Mon Sep 17 00:00:00 2001
From: Rens 
Date: Mon, 3 Mar 2025 15:01:36 +0100
Subject: [PATCH 055/182] remove transform from tour nb

---
 nbs/000_tour.ipynb | 116 ---------------------------------------------
 1 file changed, 116 deletions(-)

diff --git a/nbs/000_tour.ipynb b/nbs/000_tour.ipynb
index 15641a70..f179790f 100644
--- a/nbs/000_tour.ipynb
+++ b/nbs/000_tour.ipynb
@@ -563,122 +563,6 @@
     "1 + L(2,3,4)"
    ]
   },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "### Transforms"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "A `Transform` is the main building block of the fastai data pipelines. In the most general terms a transform can be any function you want to apply to your data, however the `Transform` class provides several mechanisms that make the process of building them easy and flexible (see the docs for information about each of these):\n",
-    "\n",
-    "- Type dispatch\n",
-    "- Dispatch over tuples\n",
-    "- Reversability\n",
-    "- Type propagation\n",
-    "- Preprocessing\n",
-    "- Filtering based on the dataset type\n",
-    "- Ordering\n",
-    "- Appending new behavior with decorators\n",
-    "\n",
-    "`Transform` looks for three special methods, encodes, decodes, and setups, which provide the implementation for [`__call__`](https://www.python-course.eu/python3_magic_methods.php), `decode`, and `setup` respectively. For instance:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "2"
-      ]
-     },
-     "execution_count": null,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "class A(Transform):\n",
-    "    def encodes(self, x): return x+1\n",
-    "\n",
-    "A()(1)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "For simple transforms like this, you can also use `Transform` as a decorator:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "2"
-      ]
-     },
-     "execution_count": null,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "@Transform\n",
-    "def f(x): return x+1\n",
-    "\n",
-    "f(1)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Transforms can be composed into a `Pipeline`:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "2.0"
-      ]
-     },
-     "execution_count": null,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "@Transform\n",
-    "def g(x): return x/2\n",
-    "\n",
-    "pipe = Pipeline([f,g])\n",
-    "pipe(3)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "The power of `Transform` and `Pipeline` is best understood by seeing how they're used to create a complete data processing pipeline. This is explained in [chapter 11](https://github.com/fastai/fastbook/blob/master/11_midlevel_data.ipynb) of the [fastai book](https://www.amazon.com/Deep-Learning-Coders-fastai-PyTorch/dp/1492045527), which is [available for free](https://github.com/fastai/fastbook) in Jupyter Notebook format."
-   ]
-  },
   {
    "cell_type": "code",
    "execution_count": null,

From 3850bc2ecb252aa0315dc7ecd1c9cad348d4bdb4 Mon Sep 17 00:00:00 2001
From: Rens 
Date: Mon, 3 Mar 2025 15:15:37 +0100
Subject: [PATCH 056/182] update llms.txt

---
 nbs/llms-ctx-full.txt | 270 +----------------------------------------
 nbs/llms-ctx.txt      | 275 +-----------------------------------------
 nbs/llms.txt          |   3 +-
 3 files changed, 9 insertions(+), 539 deletions(-)

diff --git a/nbs/llms-ctx-full.txt b/nbs/llms-ctx-full.txt
index 793c9597..c87a81b9 100644
--- a/nbs/llms-ctx-full.txt
+++ b/nbs/llms-ctx-full.txt
@@ -1,5 +1,5 @@
 
-fastcore adds to Python features inspired by other languages, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type.
+fastcore adds to Python features inspired by other languages, like mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type.
 
 Here are some tips on using fastcore:
 
@@ -12,7 +12,6 @@ Here are some tips on using fastcore:
 - **Expressive testing**: Prefer fastcore's testing functions like `test_eq`, `test_ne`, `test_close` for more readable and informative test assertions.
 - **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to `pathlib.Path`.
 - **Flexible data structures**: Convert between dictionaries and attribute-access objects using `dict2obj` and `obj2dict` for more intuitive data handling.
-- **Data pipeline construction**: Employ `Transform` and `Pipeline` classes to create modular, composable data processing workflows.
 - **Functional programming paradigms**: Utilize tools like `compose`, `maps`, and `filter_ex` to write more functional-style Python code.
 - **Documentation**: Use `docments` where possible to document parameters of functions and methods.
 - **Time-aware caching**: Apply the `timed_cache` decorator to add time-based expiration to the standard `lru_cache` functionality.
@@ -364,77 +363,8 @@ in `list` in the first place, such as making this do what you’d expect
 
     (#4) [1,2,3,4]
 
-### Transforms
-
-A [`Transform`](https://fastcore.fast.ai/transform.html#transform) is
-the main building block of the fastai data pipelines. In the most
-general terms a transform can be any function you want to apply to your
-data, however the
-[`Transform`](https://fastcore.fast.ai/transform.html#transform) class
-provides several mechanisms that make the process of building them easy
-and flexible (see the docs for information about each of these):
-
-- Type dispatch
-- Dispatch over tuples
-- Reversability
-- Type propagation
-- Preprocessing
-- Filtering based on the dataset type
-- Ordering
-- Appending new behavior with decorators
-
-[`Transform`](https://fastcore.fast.ai/transform.html#transform) looks
-for three special methods, encodes, decodes,
-and setups, which provide the implementation for
-[`__call__`](https://www.python-course.eu/python3_magic_methods.php),
-`decode`, and `setup` respectively. For instance:
-
-``` python
-class A(Transform):
-    def encodes(self, x): return x+1
-
-A()(1)
-```
-
-    2
-
-For simple transforms like this, you can also use
-[`Transform`](https://fastcore.fast.ai/transform.html#transform) as a
-decorator:
-
-``` python
-@Transform
-def f(x): return x+1
-
-f(1)
-```
-
-    2
-
-Transforms can be composed into a
-[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline):
-
-``` python
-@Transform
-def g(x): return x/2
-
-pipe = Pipeline([f,g])
-pipe(3)
-```
-
-    2.0
-
-The power of
-[`Transform`](https://fastcore.fast.ai/transform.html#transform) and
-[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) is best
-understood by seeing how they’re used to create a complete data
-processing pipeline. This is explained in [chapter
-11](https://github.com/fastai/fastbook/blob/master/11_midlevel_data.ipynb)
-of the [fastai
-book](https://www.amazon.com/Deep-Learning-Coders-fastai-PyTorch/dp/1492045527),
-which is [available for free](https://github.com/fastai/fastbook) in
-Jupyter Notebook format.
-    # fastcore: An Underrated Python Library
+
+# fastcore: An Underrated Python Library
 
 A unique python library that extends the python programming language and provides utilities that enhance productivity.
 
@@ -627,76 +557,6 @@ assert sc.some_attr == 'hello'
 
 * * *
 
-## Type Dispatch __
-
-Type dispatch, orMultiple dispatch, allows you to change the way a function behaves based upon the input types it receives. This is a prominent feature in some programming languages like Julia. For example, this is a conceptual example of how multiple dispatch works in Julia, returning different values depending on the input types of x and y:
-
-```
-collide_with(x::Asteroid, y::Asteroid) = ... 
-# deal with asteroid hitting asteroid
-
-collide_with(x::Asteroid, y::Spaceship) = ... 
-# deal with asteroid hitting spaceship
-
-collide_with(x::Spaceship, y::Asteroid) = ... 
-# deal with spaceship hitting asteroid
-
-collide_with(x::Spaceship, y::Spaceship) = ... 
-# deal with spaceship hitting spaceship
-
-```
-
-Type dispatch can be especially useful in data science, where you might allow different input types (i.e. Numpy arrays and Pandas dataframes) to a function that processes data. Type dispatch allows you to have a common API for functions that do similar tasks.
-
-Unfortunately, Python does not support this out-of-the box. Fortunately, there is the @typedispatch decorator to the rescue. This decorator relies upon type hints in order to route inputs the correct version of the function:
-
-```
-@typedispatch
-def f(x:str, y:str): return f'{x}{y}'
-
-@typedispatch
-def f(x:np.ndarray): return x.sum()
-
-@typedispatch
-def f(x:int, y:int): return x+y
-
-```
-
-Below is a demonstration of type dispatch at work for the function `f`:
-
-```
-f('Hello ', 'World!')
-
-```
-
-```
-'Hello World!'
-```
-
-```
-f(2,3)
-
-```
-
-```
-5
-```
-
-```
-f(np.array([5,5,5,5]))
-
-```
-
-```
-20
-```
-
-There are limitations of this feature, as well as other ways of using this functionality that you can read about here. In the process of learning about typed dispatch, I also found a python library called multipledispatch made by Mathhew Rocklin (the creator of Dask).
-
-After using this feature, I am now motivated to learn languages like Julia to discover what other paradigms I might be missing.
-
-* * *
-
 ## A better version of functools.partial __
 
 `functools.partial` is a great utility that creates functions from other functions that lets you set default values. Lets take this function for example that filters a list to only contain values >= `val`:
@@ -1050,10 +910,6 @@ Thefunctional programming section is my favorite part of this library.
   * mapped: A more robust `map`
   * using_attr: compose a function that operates on an attribute
 
-## Transforms __
-
-Transforms is a collection of utilities for creating data transformations and associated pipelines. These transformation utilities build upon many of the building blocks discussed in this blog post.
-
 ## Further Reading __
 
 **It should be noted that you should read themain page of the docs first, followed by the section on tests to fully understand the documentation.**
@@ -1493,60 +1349,6 @@ This blog post was written entirely in a Jupyter Notebook, which GitHub automati
 - `def str2bool(s)`
     Case-insensitive convert string `s` too a bool (`y`,`yes`,`t`,`true`,`on`,`1`->`True`)
 
-## fastcore.dispatch
-
-> Basic single and dual parameter dispatch
-
-- `def lenient_issubclass(cls, types)`
-    If possible return whether `cls` is a subclass of `types`, otherwise return False.
-
-- `def sorted_topologically(iterable)`
-    Return a new list containing all items from the iterable sorted topologically
-
-- `class TypeDispatch`
-    Dictionary-like object; `__getitem__` matches keys of types using `issubclass`
-
-    - `def __init__(self, funcs, bases)`
-    - `def add(self, f)`
-        Add type `t` and function `f`
-
-    - `def first(self)`
-        Get first function in ordered dict of type:func.
-
-    - `def returns(self, x)`
-        Get the return type of annotation of `x`.
-
-    - `def __repr__(self)`
-    - `def __call__(self, *args, **kwargs)`
-    - `def __get__(self, inst, owner)`
-    - `def __getitem__(self, k)`
-        Find first matching type that is a super-class of `k`
-
-
-- `class DispatchReg`
-    A global registry for `TypeDispatch` objects keyed by function name
-
-    - `def __init__(self)`
-    - `def __call__(self, f)`
-
-- `def retain_meta(x, res, as_copy)`
-    Call `res.set_meta(x)`, if it exists
-
-- `def default_set_meta(self, x, as_copy)`
-    Copy over `_meta` from `x` to `res`, if it's missing
-
-- `@typedispatch def cast(x, typ)`
-    cast `x` to type `typ` (may also change `x` inplace)
-
-- `def retain_type(new, old, typ, as_copy)`
-    Cast `new` to type of `old` or `typ` if it's a superclass
-
-- `def retain_types(new, old, typs)`
-    Cast each item of `new` to type of matching item in `old` if it's a superclass
-
-- `def explode_types(o)`
-    Return the type of `o`, potentially in nested dictionaries for thing that are listy
-
 ## fastcore.docments
 
 > Document parameters using comments.
@@ -2056,72 +1858,6 @@ This blog post was written entirely in a Jupyter Notebook, which GitHub automati
     - `def __enter__(self)`
     - `def __exit__(self, type, value, traceback)`
 
-## fastcore.transform
-
-> Definition of `Transform` and `Pipeline`
-
-- `class Transform`
-    Delegates (`__call__`,`decode`,`setup`) to (encodes,decodes,setups) if `split_idx` matches
-
-    - `def __init__(self, enc, dec, split_idx, order)`
-    - `@property def name(self)`
-    - `def __call__(self, x, **kwargs)`
-    - `def decode(self, x, **kwargs)`
-    - `def __repr__(self)`
-    - `def setup(self, items, train_setup)`
-
-- `class InplaceTransform`
-    A `Transform` that modifies in-place and just returns whatever it's passed
-
-
-- `class DisplayedTransform`
-    A transform with a `__repr__` that shows its attrs
-
-    - `@property def name(self)`
-
-- `class ItemTransform`
-    A transform that always take tuples as items
-
-    - `def __call__(self, x, **kwargs)`
-    - `def decode(self, x, **kwargs)`
-
-- `def get_func(t, name, *args, **kwargs)`
-    Get the `t.name` (potentially partial-ized with `args` and `kwargs`) or `noop` if not defined
-
-- `class Func`
-    Basic wrapper around a `name` with `args` and `kwargs` to call on a given type
-
-    - `def __init__(self, name, *args, **kwargs)`
-    - `def __repr__(self)`
-    - `def __call__(self, t)`
-
-- `def compose_tfms(x, tfms, is_enc, reverse, **kwargs)`
-    Apply all `func_nm` attribute of `tfms` on `x`, maybe in `reverse` order
-
-- `def mk_transform(f)`
-    Convert function `f` to `Transform` if it isn't already one
-
-- `def gather_attrs(o, k, nm)`
-    Used in __getattr__ to collect all attrs `k` from `self.{nm}`
-
-- `def gather_attr_names(o, nm)`
-    Used in __dir__ to collect all attrs `k` from `self.{nm}`
-
-- `class Pipeline`
-    A pipeline of composed (for encode/decode) transforms, setup with types
-
-    - `def __init__(self, funcs, split_idx)`
-    - `def setup(self, items, train_setup)`
-    - `def add(self, ts, items, train_setup)`
-    - `def __call__(self, o)`
-    - `def __repr__(self)`
-    - `def __getitem__(self, i)`
-    - `def __setstate__(self, data)`
-    - `def __getattr__(self, k)`
-    - `def __dir__(self)`
-    - `def decode(self, o, full)`
-    - `def show(self, o, ctx, **kwargs)`
-
 ## fastcore.xdg
 
 > XDG Base Directory Specification helpers.
diff --git a/nbs/llms-ctx.txt b/nbs/llms-ctx.txt
index e9c0506c..b090f512 100644
--- a/nbs/llms-ctx.txt
+++ b/nbs/llms-ctx.txt
@@ -1,5 +1,5 @@
 
-fastcore adds to Python features inspired by other languages, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type.
+fastcore adds to Python features inspired by other languages, like mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type.
 
 Here are some tips on using fastcore:
 
@@ -12,7 +12,6 @@ Here are some tips on using fastcore:
 - **Expressive testing**: Prefer fastcore's testing functions like `test_eq`, `test_ne`, `test_close` for more readable and informative test assertions.
 - **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to `pathlib.Path`.
 - **Flexible data structures**: Convert between dictionaries and attribute-access objects using `dict2obj` and `obj2dict` for more intuitive data handling.
-- **Data pipeline construction**: Employ `Transform` and `Pipeline` classes to create modular, composable data processing workflows.
 - **Functional programming paradigms**: Utilize tools like `compose`, `maps`, and `filter_ex` to write more functional-style Python code.
 - **Documentation**: Use `docments` where possible to document parameters of functions and methods.
 - **Time-aware caching**: Apply the `timed_cache` decorator to add time-based expiration to the standard `lru_cache` functionality.
@@ -363,78 +362,8 @@ in `list` in the first place, such as making this do what you’d expect
 ```
 
     (#4) [1,2,3,4]
-
-### Transforms
-
-A [`Transform`](https://fastcore.fast.ai/transform.html#transform) is
-the main building block of the fastai data pipelines. In the most
-general terms a transform can be any function you want to apply to your
-data, however the
-[`Transform`](https://fastcore.fast.ai/transform.html#transform) class
-provides several mechanisms that make the process of building them easy
-and flexible (see the docs for information about each of these):
-
-- Type dispatch
-- Dispatch over tuples
-- Reversability
-- Type propagation
-- Preprocessing
-- Filtering based on the dataset type
-- Ordering
-- Appending new behavior with decorators
-
-[`Transform`](https://fastcore.fast.ai/transform.html#transform) looks
-for three special methods, encodes, decodes,
-and setups, which provide the implementation for
-[`__call__`](https://www.python-course.eu/python3_magic_methods.php),
-`decode`, and `setup` respectively. For instance:
-
-``` python
-class A(Transform):
-    def encodes(self, x): return x+1
-
-A()(1)
-```
-
-    2
-
-For simple transforms like this, you can also use
-[`Transform`](https://fastcore.fast.ai/transform.html#transform) as a
-decorator:
-
-``` python
-@Transform
-def f(x): return x+1
-
-f(1)
-```
-
-    2
-
-Transforms can be composed into a
-[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline):
-
-``` python
-@Transform
-def g(x): return x/2
-
-pipe = Pipeline([f,g])
-pipe(3)
-```
-
-    2.0
-
-The power of
-[`Transform`](https://fastcore.fast.ai/transform.html#transform) and
-[`Pipeline`](https://fastcore.fast.ai/transform.html#pipeline) is best
-understood by seeing how they’re used to create a complete data
-processing pipeline. This is explained in [chapter
-11](https://github.com/fastai/fastbook/blob/master/11_midlevel_data.ipynb)
-of the [fastai
-book](https://www.amazon.com/Deep-Learning-Coders-fastai-PyTorch/dp/1492045527),
-which is [available for free](https://github.com/fastai/fastbook) in
-Jupyter Notebook format.
-    # fastcore: An Underrated Python Library
+
+# fastcore: An Underrated Python Library
 
 A unique python library that extends the python programming language and provides utilities that enhance productivity.
 
@@ -627,76 +556,6 @@ assert sc.some_attr == 'hello'
 
 * * *
 
-## Type Dispatch __
-
-Type dispatch, orMultiple dispatch, allows you to change the way a function behaves based upon the input types it receives. This is a prominent feature in some programming languages like Julia. For example, this is a conceptual example of how multiple dispatch works in Julia, returning different values depending on the input types of x and y:
-
-```
-collide_with(x::Asteroid, y::Asteroid) = ... 
-# deal with asteroid hitting asteroid
-
-collide_with(x::Asteroid, y::Spaceship) = ... 
-# deal with asteroid hitting spaceship
-
-collide_with(x::Spaceship, y::Asteroid) = ... 
-# deal with spaceship hitting asteroid
-
-collide_with(x::Spaceship, y::Spaceship) = ... 
-# deal with spaceship hitting spaceship
-
-```
-
-Type dispatch can be especially useful in data science, where you might allow different input types (i.e. Numpy arrays and Pandas dataframes) to a function that processes data. Type dispatch allows you to have a common API for functions that do similar tasks.
-
-Unfortunately, Python does not support this out-of-the box. Fortunately, there is the @typedispatch decorator to the rescue. This decorator relies upon type hints in order to route inputs the correct version of the function:
-
-```
-@typedispatch
-def f(x:str, y:str): return f'{x}{y}'
-
-@typedispatch
-def f(x:np.ndarray): return x.sum()
-
-@typedispatch
-def f(x:int, y:int): return x+y
-
-```
-
-Below is a demonstration of type dispatch at work for the function `f`:
-
-```
-f('Hello ', 'World!')
-
-```
-
-```
-'Hello World!'
-```
-
-```
-f(2,3)
-
-```
-
-```
-5
-```
-
-```
-f(np.array([5,5,5,5]))
-
-```
-
-```
-20
-```
-
-There are limitations of this feature, as well as other ways of using this functionality that you can read about here. In the process of learning about typed dispatch, I also found a python library called multipledispatch made by Mathhew Rocklin (the creator of Dask).
-
-After using this feature, I am now motivated to learn languages like Julia to discover what other paradigms I might be missing.
-
-* * *
-
 ## A better version of functools.partial __
 
 `functools.partial` is a great utility that creates functions from other functions that lets you set default values. Lets take this function for example that filters a list to only contain values >= `val`:
@@ -1036,7 +895,7 @@ TheBasics section contain many shortcuts to perform common tasks or provide an a
 
 ## Multiprocessing __
 
-TheMultiprocessing section extends python's multiprocessing library by offering features like:
+The Multiprocessing section extends python's multiprocessing library by offering features like:
 
   * progress bars
   * ability to pause to mitigate race conditions with external services
@@ -1044,16 +903,12 @@ TheMultiprocessing section extends python's multiprocessing library by offering
 
 ## Functional Programming __
 
-Thefunctional programming section is my favorite part of this library.
+The functional programming section is my favorite part of this library.
 
   * maps: a map that also composes functions
   * mapped: A more robust `map`
   * using_attr: compose a function that operates on an attribute
 
-## Transforms __
-
-Transforms is a collection of utilities for creating data transformations and associated pipelines. These transformation utilities build upon many of the building blocks discussed in this blog post.
-
 ## Further Reading __
 
 **It should be noted that you should read themain page of the docs first, followed by the section on tests to fully understand the documentation.**
@@ -1493,60 +1348,6 @@ This blog post was written entirely in a Jupyter Notebook, which GitHub automati
 - `def str2bool(s)`
     Case-insensitive convert string `s` too a bool (`y`,`yes`,`t`,`true`,`on`,`1`->`True`)
 
-## fastcore.dispatch
-
-> Basic single and dual parameter dispatch
-
-- `def lenient_issubclass(cls, types)`
-    If possible return whether `cls` is a subclass of `types`, otherwise return False.
-
-- `def sorted_topologically(iterable)`
-    Return a new list containing all items from the iterable sorted topologically
-
-- `class TypeDispatch`
-    Dictionary-like object; `__getitem__` matches keys of types using `issubclass`
-
-    - `def __init__(self, funcs, bases)`
-    - `def add(self, f)`
-        Add type `t` and function `f`
-
-    - `def first(self)`
-        Get first function in ordered dict of type:func.
-
-    - `def returns(self, x)`
-        Get the return type of annotation of `x`.
-
-    - `def __repr__(self)`
-    - `def __call__(self, *args, **kwargs)`
-    - `def __get__(self, inst, owner)`
-    - `def __getitem__(self, k)`
-        Find first matching type that is a super-class of `k`
-
-
-- `class DispatchReg`
-    A global registry for `TypeDispatch` objects keyed by function name
-
-    - `def __init__(self)`
-    - `def __call__(self, f)`
-
-- `def retain_meta(x, res, as_copy)`
-    Call `res.set_meta(x)`, if it exists
-
-- `def default_set_meta(self, x, as_copy)`
-    Copy over `_meta` from `x` to `res`, if it's missing
-
-- `@typedispatch def cast(x, typ)`
-    cast `x` to type `typ` (may also change `x` inplace)
-
-- `def retain_type(new, old, typ, as_copy)`
-    Cast `new` to type of `old` or `typ` if it's a superclass
-
-- `def retain_types(new, old, typs)`
-    Cast each item of `new` to type of matching item in `old` if it's a superclass
-
-- `def explode_types(o)`
-    Return the type of `o`, potentially in nested dictionaries for thing that are listy
-
 ## fastcore.docments
 
 > Document parameters using comments.
@@ -2056,72 +1857,6 @@ This blog post was written entirely in a Jupyter Notebook, which GitHub automati
     - `def __enter__(self)`
     - `def __exit__(self, type, value, traceback)`
 
-## fastcore.transform
-
-> Definition of `Transform` and `Pipeline`
-
-- `class Transform`
-    Delegates (`__call__`,`decode`,`setup`) to (encodes,decodes,setups) if `split_idx` matches
-
-    - `def __init__(self, enc, dec, split_idx, order)`
-    - `@property def name(self)`
-    - `def __call__(self, x, **kwargs)`
-    - `def decode(self, x, **kwargs)`
-    - `def __repr__(self)`
-    - `def setup(self, items, train_setup)`
-
-- `class InplaceTransform`
-    A `Transform` that modifies in-place and just returns whatever it's passed
-
-
-- `class DisplayedTransform`
-    A transform with a `__repr__` that shows its attrs
-
-    - `@property def name(self)`
-
-- `class ItemTransform`
-    A transform that always take tuples as items
-
-    - `def __call__(self, x, **kwargs)`
-    - `def decode(self, x, **kwargs)`
-
-- `def get_func(t, name, *args, **kwargs)`
-    Get the `t.name` (potentially partial-ized with `args` and `kwargs`) or `noop` if not defined
-
-- `class Func`
-    Basic wrapper around a `name` with `args` and `kwargs` to call on a given type
-
-    - `def __init__(self, name, *args, **kwargs)`
-    - `def __repr__(self)`
-    - `def __call__(self, t)`
-
-- `def compose_tfms(x, tfms, is_enc, reverse, **kwargs)`
-    Apply all `func_nm` attribute of `tfms` on `x`, maybe in `reverse` order
-
-- `def mk_transform(f)`
-    Convert function `f` to `Transform` if it isn't already one
-
-- `def gather_attrs(o, k, nm)`
-    Used in __getattr__ to collect all attrs `k` from `self.{nm}`
-
-- `def gather_attr_names(o, nm)`
-    Used in __dir__ to collect all attrs `k` from `self.{nm}`
-
-- `class Pipeline`
-    A pipeline of composed (for encode/decode) transforms, setup with types
-
-    - `def __init__(self, funcs, split_idx)`
-    - `def setup(self, items, train_setup)`
-    - `def add(self, ts, items, train_setup)`
-    - `def __call__(self, o)`
-    - `def __repr__(self)`
-    - `def __getitem__(self, i)`
-    - `def __setstate__(self, data)`
-    - `def __getattr__(self, k)`
-    - `def __dir__(self)`
-    - `def decode(self, o, full)`
-    - `def show(self, o, ctx, **kwargs)`
-
 ## fastcore.xdg
 
 > XDG Base Directory Specification helpers.
diff --git a/nbs/llms.txt b/nbs/llms.txt
index 034ac932..acd7c30f 100644
--- a/nbs/llms.txt
+++ b/nbs/llms.txt
@@ -1,6 +1,6 @@
 # fastcore
 
-fastcore adds to Python features inspired by other languages, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some “missing features” and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python’s list type.
+fastcore adds to Python features inspired by other languages, like mixins from Ruby, and currying, binding, and more from Haskell. It also adds some "missing features" and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python's list type.
 
 Here are some tips on using fastcore:
 
@@ -13,7 +13,6 @@ Here are some tips on using fastcore:
 - **Expressive testing**: Prefer fastcore's testing functions like `test_eq`, `test_ne`, `test_close` for more readable and informative test assertions.
 - **Advanced file operations**: Use the extended `Path` class, which adds methods like `ls()`, `read_json()`, and others to `pathlib.Path`.
 - **Flexible data structures**: Convert between dictionaries and attribute-access objects using `dict2obj` and `obj2dict` for more intuitive data handling.
-- **Data pipeline construction**: Employ `Transform` and `Pipeline` classes to create modular, composable data processing workflows.
 - **Functional programming paradigms**: Utilize tools like `compose`, `maps`, and `filter_ex` to write more functional-style Python code.
 - **Documentation**: Use `docments` where possible to document parameters of functions and methods.
 - **Time-aware caching**: Apply the `timed_cache` decorator to add time-based expiration to the standard `lru_cache` functionality.

From a3d565e07feb177980cd288217c7b5db742136a9 Mon Sep 17 00:00:00 2001
From: Rens 
Date: Mon, 3 Mar 2025 15:26:02 +0100
Subject: [PATCH 057/182] update version to 1.8.0 so latest fastai does not use
 this version yet.

---
 fastcore/__init__.py | 2 +-
 settings.ini         | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/fastcore/__init__.py b/fastcore/__init__.py
index f3ce51b7..29654eec 100644
--- a/fastcore/__init__.py
+++ b/fastcore/__init__.py
@@ -1 +1 @@
-__version__ = "1.7.30"
+__version__ = "1.8.0"
diff --git a/settings.ini b/settings.ini
index 0c104996..86e55791 100644
--- a/settings.ini
+++ b/settings.ini
@@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger
 author_email = infos@fast.ai
 copyright = fast.ai
 branch = master
-version = 1.7.30
+version = 1.8.0
 min_python = 3.9
 audience = Developers
 language = English
@@ -32,7 +32,6 @@ black_formatting = False
 readme_nb = index.ipynb
 allowed_metadata_keys = 
 allowed_cell_metadata_keys = 
-jupyter_hooks = True
 clean_ids = False
 conda_user = fastai
 clear_all = False

From 1d60a29fc1f7f348e824d53b17d507387dc5bcfc Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Sat, 8 Mar 2025 09:07:23 +1000
Subject: [PATCH 058/182] fixes #671

---
 fastcore/docments.py  | 6 ++++--
 nbs/06_docments.ipynb | 6 ++++--
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/fastcore/docments.py b/fastcore/docments.py
index f68a5b3d..654656ba 100644
--- a/fastcore/docments.py
+++ b/fastcore/docments.py
@@ -48,16 +48,18 @@ def get_dataclass_source(s):
 # %% ../nbs/06_docments.ipynb
 def get_source(s):
     "Get source code for string, function object or dataclass `s`"
-    return getsource(s) if isfunction(s) or ismethod(s) else get_dataclass_source(s) if isdataclass(s) else s
+    if isinstance(s,str): return s
+    return getsource(s) if isfunction(s) or ismethod(s) else get_dataclass_source(s) if isdataclass(s) else None
 
 # %% ../nbs/06_docments.ipynb
 def _parses(s):
     "Parse Python code in string, function object or dataclass `s`"
-    return parse(dedent(get_source(s)))
+    return parse(dedent(get_source(s) or ''))
 
 def _tokens(s):
     "Tokenize Python code in string or function object `s`"
     s = get_source(s)
+    if not s: return []
     return tokenize(BytesIO(s.encode('utf-8')).readline)
 
 _clean_re = re.compile(r'^\s*#(.*)\s*$')
diff --git a/nbs/06_docments.ipynb b/nbs/06_docments.ipynb
index 6c2eac6b..21817d59 100644
--- a/nbs/06_docments.ipynb
+++ b/nbs/06_docments.ipynb
@@ -213,7 +213,8 @@
     "#|export\n",
     "def get_source(s):\n",
     "    \"Get source code for string, function object or dataclass `s`\"\n",
-    "    return getsource(s) if isfunction(s) or ismethod(s) else get_dataclass_source(s) if isdataclass(s) else s"
+    "    if isinstance(s,str): return s\n",
+    "    return getsource(s) if isfunction(s) or ismethod(s) else get_dataclass_source(s) if isdataclass(s) else None"
    ]
   },
   {
@@ -225,11 +226,12 @@
     "#|export\n",
     "def _parses(s):\n",
     "    \"Parse Python code in string, function object or dataclass `s`\"\n",
-    "    return parse(dedent(get_source(s)))\n",
+    "    return parse(dedent(get_source(s) or ''))\n",
     "\n",
     "def _tokens(s):\n",
     "    \"Tokenize Python code in string or function object `s`\"\n",
     "    s = get_source(s)\n",
+    "    if not s: return []\n",
     "    return tokenize(BytesIO(s.encode('utf-8')).readline)\n",
     "\n",
     "_clean_re = re.compile(r'^\\s*#(.*)\\s*$')\n",

From cdd6ed96ff444069fd01063fb9e1e45bbbbc34df Mon Sep 17 00:00:00 2001
From: Rens 
Date: Mon, 17 Mar 2025 13:17:16 +0100
Subject: [PATCH 059/182] add plum-dispatch to dev-deps and add test to xtras

---
 fastcore/_modidx.py |  1 -
 fastcore/xtras.py   |  6 ++++--
 nbs/03_xtras.ipynb  | 36 ++++++++++++++++++++++++++++++++++--
 settings.ini        |  3 +--
 4 files changed, 39 insertions(+), 7 deletions(-)

diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py
index f5a3ea27..946c7380 100644
--- a/fastcore/_modidx.py
+++ b/fastcore/_modidx.py
@@ -578,7 +578,6 @@
                                 'fastcore.xtras._ceil': ('xtras.html#_ceil', 'fastcore/xtras.py'),
                                 'fastcore.xtras._has_property_getter': ('xtras.html#_has_property_getter', 'fastcore/xtras.py'),
                                 'fastcore.xtras._is_property': ('xtras.html#_is_property', 'fastcore/xtras.py'),
-                                'fastcore.xtras._is_type_dispatch': ('xtras.html#_is_type_dispatch', 'fastcore/xtras.py'),
                                 'fastcore.xtras._property_getter': ('xtras.html#_property_getter', 'fastcore/xtras.py'),
                                 'fastcore.xtras._repr_dict': ('xtras.html#_repr_dict', 'fastcore/xtras.py'),
                                 'fastcore.xtras._sparkchar': ('xtras.html#_sparkchar', 'fastcore/xtras.py'),
diff --git a/fastcore/xtras.py b/fastcore/xtras.py
index 9d52bffa..5d76c0c7 100644
--- a/fastcore/xtras.py
+++ b/fastcore/xtras.py
@@ -429,9 +429,11 @@ def exec_eval(code,   # Code to exec/eval
     else: exec(code, g, l)
 
 # %% ../nbs/03_xtras.ipynb
-def _is_type_dispatch(x): return type(x).__name__ == "Function"  # assumes plum-dispatch library is used
-def _unwrapped_type_dispatch_func(x): return x.methods[0].implementation if _is_type_dispatch(x) else x
+def _unwrapped_type_dispatch_func(x): 
+    # use isinstance_str to avoid adding plum-dispatch as dependency to fastcore
+    return x.methods[0].implementation if isinstance_str(x,"Function") else x
 
+# %% ../nbs/03_xtras.ipynb
 def _is_property(x): return type(x)==property
 def _has_property_getter(x): return _is_property(x) and hasattr(x, 'fget') and hasattr(x.fget, 'func')
 def _property_getter(x): return x.fget.func if _has_property_getter(x) else x
diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb
index 7f91bef0..73bdef45 100644
--- a/nbs/03_xtras.ipynb
+++ b/nbs/03_xtras.ipynb
@@ -1896,9 +1896,41 @@
    "outputs": [],
    "source": [
     "#|export\n",
-    "def _is_type_dispatch(x): return type(x).__name__ == \"Function\"  # assumes plum-dispatch library is used\n",
-    "def _unwrapped_type_dispatch_func(x): return x.methods[0].implementation if _is_type_dispatch(x) else x\n",
+    "def _unwrapped_type_dispatch_func(x): \n",
+    "    # use isinstance_str to avoid adding plum-dispatch as dependency to fastcore\n",
+    "    return x.methods[0].implementation if isinstance_str(x,\"Function\") else x"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This function helps us identify the first declared raw function of a dispatched function:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from plum import Function\n",
+    "\n",
+    "def f1(x): return \"Any\"\n",
+    "def f2(x:int): return \"Int\"\n",
+    "\n",
+    "df = Function(f1).dispatch(f1).dispatch(f2)\n",
     "\n",
+    "test_eq(_unwrapped_type_dispatch_func(df), f1)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "#|export\n",
     "def _is_property(x): return type(x)==property\n",
     "def _has_property_getter(x): return _is_property(x) and hasattr(x, 'fget') and hasattr(x.fget, 'func')\n",
     "def _property_getter(x): return x.fget.func if _has_property_getter(x) else x\n",
diff --git a/settings.ini b/settings.ini
index 86e55791..40921d44 100644
--- a/settings.ini
+++ b/settings.ini
@@ -17,7 +17,7 @@ license = apache2
 status = 5
 nbs_path = nbs
 doc_path = _docs
-dev_requirements = numpy nbdev>=0.2.39 matplotlib pillow torch pandas nbclassic pysymbol_llm llms-txt
+dev_requirements = numpy nbdev>=0.2.39 matplotlib pillow torch pandas nbclassic pysymbol_llm llms-txt plum-dispatch
 git_url = https://github.com/AnswerDotAI/fastcore/
 lib_path = fastcore
 title = fastcore
@@ -38,4 +38,3 @@ clear_all = False
 put_version_in_init = True
 cell_number = False
 skip_procs = 
-

From 90b623224e9fd666a20d505333b9bcc4f4edfef4 Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Tue, 18 Mar 2025 15:20:35 +1000
Subject: [PATCH 060/182] release

---
 CHANGELOG.md | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ae00b98..7687e80d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
 
 
 
+## 1.8.0
+
+### Breaking Changes
+
+- Move dispatch and transform modules to fasttransform ([#669](https://github.com/AnswerDotAI/fastcore/pull/669)), thanks to [@RensDimmendaal](https://github.com/RensDimmendaal)
+
+### Bugs Squashed
+
+- `list[object]` not handled correctly in docments ([#671](https://github.com/AnswerDotAI/fastcore/issues/671))
+
+
 ## 1.7.29
 
 ### New Features

From 99dc143a439acf2b1bb1f2b728b519281c280aa1 Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Tue, 18 Mar 2025 15:20:51 +1000
Subject: [PATCH 061/182] bump

---
 fastcore/__init__.py | 2 +-
 settings.ini         | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/fastcore/__init__.py b/fastcore/__init__.py
index 29654eec..2d986fc5 100644
--- a/fastcore/__init__.py
+++ b/fastcore/__init__.py
@@ -1 +1 @@
-__version__ = "1.8.0"
+__version__ = "1.8.1"
diff --git a/settings.ini b/settings.ini
index 40921d44..78a34db7 100644
--- a/settings.ini
+++ b/settings.ini
@@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger
 author_email = infos@fast.ai
 copyright = fast.ai
 branch = master
-version = 1.8.0
+version = 1.8.1
 min_python = 3.9
 audience = Developers
 language = English
@@ -38,3 +38,5 @@ clear_all = False
 put_version_in_init = True
 cell_number = False
 skip_procs = 
+jupyter_hooks = False
+

From f2ee316e2e678e15360bceabb566908e4f725675 Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Tue, 18 Mar 2025 15:48:34 +1000
Subject: [PATCH 062/182] Update main.yml

---
 .github/workflows/main.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index cccffa95..6cf14c67 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -80,7 +80,7 @@ jobs:
     - name: Set up Python
       uses: actions/setup-python@v3
       with:
-        python-version: '3.9'
+        python-version: '3.11'
     - name: clone this branch [fastcore]
       uses: actions/checkout@v3
       with:

From cdc5e5061ac4a6e2eb163b150ba9437a94b6a0ab Mon Sep 17 00:00:00 2001
From: Jay Suh 
Date: Thu, 20 Mar 2025 00:45:17 -0500
Subject: [PATCH 063/182] Fix colab link

---
 nbs/000_tour.ipynb | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/nbs/000_tour.ipynb b/nbs/000_tour.ipynb
index f179790f..0d162645 100644
--- a/nbs/000_tour.ipynb
+++ b/nbs/000_tour.ipynb
@@ -48,7 +48,7 @@
     {
      "data": {
       "text/markdown": [
-       "[Open `index` in Colab](https://colab.research.google.com/github/fastai/fastcore/blob/master/nbs/index.ipynb)"
+       "[Open `000_tour` in Colab](https://colab.research.google.com/github/AnswerDotAI/fastcore/blob/master/nbs/000_tour.ipynb)"
       ],
       "text/plain": [
        ""
@@ -59,7 +59,7 @@
     }
    ],
    "source": [
-    "colab_link('index')"
+    "colab_link('000_tour')"
    ]
   },
   {
@@ -77,7 +77,7 @@
     {
      "data": {
       "text/plain": [
-       ""
+       ""
       ]
      },
      "execution_count": null,
@@ -357,7 +357,7 @@
     {
      "data": {
       "text/plain": [
-       "__main__.ProductPage(author='Jeremy', price=1.5, cost=0.5)"
+       "ProductPage(author='Jeremy', price=1.5, cost=0.5)"
       ]
      },
      "execution_count": null,
@@ -467,7 +467,7 @@
     {
      "data": {
       "text/plain": [
-       "(#20) [5,1,9,10,18,13,6,17,3,16...]"
+       "(#20) [4,14,1,7,3,18,5,6,11,15,13,17,16,2,8,10,0,9,12,19]"
       ]
      },
      "execution_count": null,
@@ -495,7 +495,7 @@
     {
      "data": {
       "text/plain": [
-       "(#3) [9,18,6]"
+       "(#3) [1,3,5]"
       ]
      },
      "execution_count": null,
@@ -522,7 +522,7 @@
     {
      "data": {
       "text/plain": [
-       "(#5) [4,7,9,18,19]"
+       "(#5) [5,9,11,12,19]"
       ]
      },
      "execution_count": null,

From 9f6e6a84eccf31c8e1e2229d31a6720d5d1557d6 Mon Sep 17 00:00:00 2001
From: Jay Suh 
Date: Sat, 22 Mar 2025 17:15:23 -0500
Subject: [PATCH 064/182] Add a setter to patch

---
 fastcore/basics.py  |  16 ++++--
 nbs/01_basics.ipynb | 119 ++++++++++++++++++++++++++++++++------------
 2 files changed, 99 insertions(+), 36 deletions(-)

diff --git a/fastcore/basics.py b/fastcore/basics.py
index 10a553e2..7d2503a1 100644
--- a/fastcore/basics.py
+++ b/fastcore/basics.py
@@ -1034,7 +1034,7 @@ def __init__(self, f): self.f = f
     def __get__(self, _, f_cls): return MethodType(self.f, f_cls)
 
 # %% ../nbs/01_basics.ipynb
-def patch_to(cls, as_prop=False, cls_method=False):
+def patch_to(cls, as_prop=False, cls_method=False, set_prop=False):
     "Decorator: add `f` to `cls`"
     if not isinstance(cls, (tuple,list)): cls=(cls,)
     def _inner(f):
@@ -1046,7 +1046,13 @@ def _inner(f):
             nf.__qualname__ = f"{c_.__name__}.{nm}"
             if cls_method: setattr(c_, nm, _clsmethod(nf))
             else:
-                if as_prop: setattr(c_, nm, property(nf))
+                if set_prop:
+                    if isinstance(getattr(c_, nm, None), property):
+                        prop = getattr(c_, nm)
+                        setattr(c_, nm, prop.setter(nf))
+                    else:
+                        raise AttributeError(f"Cannot set property setter: {nm} is not a property of {c_}")
+                elif as_prop: setattr(c_, nm, property(nf))
                 else:
                     onm = '_orig_'+nm
                     if hasattr(c_, nm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, nm))
@@ -1056,12 +1062,12 @@ def _inner(f):
     return _inner
 
 # %% ../nbs/01_basics.ipynb
-def patch(f=None, *, as_prop=False, cls_method=False):
+def patch(f=None, *, as_prop=False, cls_method=False,  set_prop=False):
     "Decorator: add `f` to the first parameter's class (based on f's type annotations)"
-    if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method)
+    if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop)
     ann,glb,loc = get_annotations_ex(f)
     cls = union2tuple(eval_type(ann.pop('cls') if cls_method else next(iter(ann.values())), glb, loc))
-    return patch_to(cls, as_prop=as_prop, cls_method=cls_method)(f)
+    return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop)(f)
 
 # %% ../nbs/01_basics.ipynb
 def patch_property(f):
diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb
index c8ab3f99..a20f478b 100644
--- a/nbs/01_basics.ipynb
+++ b/nbs/01_basics.ipynb
@@ -4700,7 +4700,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L829){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L861){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### fastuple\n",
        "\n",
@@ -4711,7 +4711,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L829){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L861){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### fastuple\n",
        "\n",
@@ -4785,7 +4785,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L848){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L880){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### fastuple.add\n",
        "\n",
@@ -4796,7 +4796,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L848){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L880){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### fastuple.add\n",
        "\n",
@@ -4835,7 +4835,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L844){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L876){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### fastuple.mul\n",
        "\n",
@@ -4846,7 +4846,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L844){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L876){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### fastuple.mul\n",
        "\n",
@@ -4996,7 +4996,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L875){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L907){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "### bind\n",
        "\n",
@@ -5007,7 +5007,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L875){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L907){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "### bind\n",
        "\n",
@@ -5645,7 +5645,7 @@
    "outputs": [],
    "source": [
     "#|export\n",
-    "def patch_to(cls, as_prop=False, cls_method=False):\n",
+    "def patch_to(cls, as_prop=False, cls_method=False, set_prop=False):\n",
     "    \"Decorator: add `f` to `cls`\"\n",
     "    if not isinstance(cls, (tuple,list)): cls=(cls,)\n",
     "    def _inner(f):\n",
@@ -5657,7 +5657,13 @@
     "            nf.__qualname__ = f\"{c_.__name__}.{nm}\"\n",
     "            if cls_method: setattr(c_, nm, _clsmethod(nf))\n",
     "            else:\n",
-    "                if as_prop: setattr(c_, nm, property(nf))\n",
+    "                if set_prop:\n",
+    "                    if isinstance(getattr(c_, nm, None), property):\n",
+    "                        prop = getattr(c_, nm)\n",
+    "                        setattr(c_, nm, prop.setter(nf))\n",
+    "                    else:\n",
+    "                        raise AttributeError(f\"Cannot set property setter: {nm} is not a property of {c_}\")\n",
+    "                elif as_prop: setattr(c_, nm, property(nf))\n",
     "                else:\n",
     "                    onm = '_orig_'+nm\n",
     "                    if hasattr(c_, nm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, nm))\n",
@@ -5753,6 +5759,35 @@
     "test_eq(t.add_ten, 14)"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Once you have a property, you can assign a setter with `set_prop=True`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class _T2():\n",
+    "    def __init__(self, val): self._val = val\n",
+    "\n",
+    "@patch_to(_T2, as_prop=True)\n",
+    "def val(self): return self._val\n",
+    "\n",
+    "t = _T2(2)\n",
+    "test_eq(t.val, 2)\n",
+    "\n",
+    "@patch_to(_T2, set_prop=True)\n",
+    "def val(self, val): self._val = val\n",
+    "\n",
+    "t.val = 3\n",
+    "test_eq(t.val, 3)"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -5785,12 +5820,12 @@
    "outputs": [],
    "source": [
     "#|export\n",
-    "def patch(f=None, *, as_prop=False, cls_method=False):\n",
+    "def patch(f=None, *, as_prop=False, cls_method=False,  set_prop=False):\n",
     "    \"Decorator: add `f` to the first parameter's class (based on f's type annotations)\"\n",
-    "    if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method)\n",
+    "    if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop)\n",
     "    ann,glb,loc = get_annotations_ex(f)\n",
     "    cls = union2tuple(eval_type(ann.pop('cls') if cls_method else next(iter(ann.values())), glb, loc))\n",
-    "    return patch_to(cls, as_prop=as_prop, cls_method=cls_method)(f)"
+    "    return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop)(f)"
    ]
   },
   {
@@ -5847,7 +5882,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Just like `patch_to` decorator you can use `as_prop` and `cls_method` parameters with `patch` decorator:"
+    "Just like `patch_to` decorator you can use `as_prop`, `set_prop`, and `cls_method` parameters with `patch` decorator:"
    ]
   },
   {
@@ -5863,6 +5898,28 @@
     "test_eq(t.add_ten, 14)"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "class _T2():\n",
+    "    def __init__(self, val): self._val = val\n",
+    "\n",
+    "@patch(as_prop=True)\n",
+    "def val(self:_T2): return self._val\n",
+    "\n",
+    "t = _T2(2)\n",
+    "test_eq(t.val, 2)\n",
+    "\n",
+    "@patch(set_prop=True)\n",
+    "def val(self:_T2, val): self._val = val\n",
+    "\n",
+    "t.val = 3\n",
+    "test_eq(t.val, 3)"
+   ]
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -5972,11 +6029,11 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1047){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1079){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ImportEnum\n",
        "\n",
-       ">      ImportEnum (new_class_name, names, module=None, qualname=None, type=None,\n",
+       ">      ImportEnum (value, names=None, module=None, qualname=None, type=None,\n",
        ">                  start=1, boundary=None)\n",
        "\n",
        "*An `Enum` that can have its values imported*"
@@ -5984,11 +6041,11 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1047){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1079){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ImportEnum\n",
        "\n",
-       ">      ImportEnum (new_class_name, names, module=None, qualname=None, type=None,\n",
+       ">      ImportEnum (value, names=None, module=None, qualname=None, type=None,\n",
        ">                  start=1, boundary=None)\n",
        "\n",
        "*An `Enum` that can have its values imported*"
@@ -6037,11 +6094,11 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1055){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1087){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### StrEnum\n",
        "\n",
-       ">      StrEnum (new_class_name, names, module=None, qualname=None, type=None,\n",
+       ">      StrEnum (value, names=None, module=None, qualname=None, type=None,\n",
        ">               start=1, boundary=None)\n",
        "\n",
        "*An `ImportEnum` that behaves like a `str`*"
@@ -6049,11 +6106,11 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1055){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1087){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### StrEnum\n",
        "\n",
-       ">      StrEnum (new_class_name, names, module=None, qualname=None, type=None,\n",
+       ">      StrEnum (value, names=None, module=None, qualname=None, type=None,\n",
        ">               start=1, boundary=None)\n",
        "\n",
        "*An `ImportEnum` that behaves like a `str`*"
@@ -6102,11 +6159,11 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1065){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1097){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ValEnum\n",
        "\n",
-       ">      ValEnum (new_class_name, names, module=None, qualname=None, type=None,\n",
+       ">      ValEnum (value, names=None, module=None, qualname=None, type=None,\n",
        ">               start=1, boundary=None)\n",
        "\n",
        "*An `ImportEnum` that stringifies using values*"
@@ -6114,11 +6171,11 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1065){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1097){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ValEnum\n",
        "\n",
-       ">      ValEnum (new_class_name, names, module=None, qualname=None, type=None,\n",
+       ">      ValEnum (value, names=None, module=None, qualname=None, type=None,\n",
        ">               start=1, boundary=None)\n",
        "\n",
        "*An `ImportEnum` that stringifies using values*"
@@ -6191,7 +6248,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1070){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1102){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### Stateful\n",
        "\n",
@@ -6202,7 +6259,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1070){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1102){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### Stateful\n",
        "\n",
@@ -6353,7 +6410,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1107){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PrettyString\n",
        "\n",
@@ -6364,7 +6421,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1107){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PrettyString\n",
        "\n",
@@ -6489,7 +6546,7 @@
     {
      "data": {
       "text/plain": [
-       "10"
+       "22"
       ]
      },
      "execution_count": null,

From 8ae8f63392e882635a4413f07c2991e76f6cfa2d Mon Sep 17 00:00:00 2001
From: Jay Suh 
Date: Mon, 24 Mar 2025 05:43:31 -0500
Subject: [PATCH 065/182] Condense into a line.

Co-authored-by: Jeremy Howard 
---
 fastcore/basics.py | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/fastcore/basics.py b/fastcore/basics.py
index 7d2503a1..6c03e859 100644
--- a/fastcore/basics.py
+++ b/fastcore/basics.py
@@ -1046,12 +1046,7 @@ def _inner(f):
             nf.__qualname__ = f"{c_.__name__}.{nm}"
             if cls_method: setattr(c_, nm, _clsmethod(nf))
             else:
-                if set_prop:
-                    if isinstance(getattr(c_, nm, None), property):
-                        prop = getattr(c_, nm)
-                        setattr(c_, nm, prop.setter(nf))
-                    else:
-                        raise AttributeError(f"Cannot set property setter: {nm} is not a property of {c_}")
+                if set_prop: setattr(c_, nm, getattr(c_, nm).setter(nf))
                 elif as_prop: setattr(c_, nm, property(nf))
                 else:
                     onm = '_orig_'+nm

From cc4109d900fe87a009320a9fc6c3a0fda2b1bcd9 Mon Sep 17 00:00:00 2001
From: Jay Suh 
Date: Thu, 27 Mar 2025 17:13:58 -0500
Subject: [PATCH 066/182] Remove exceptions

---
 nbs/01_basics.ipynb | 925 ++++++++++++++++++++++----------------------
 1 file changed, 466 insertions(+), 459 deletions(-)

diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb
index a20f478b..55c0ba2f 100644
--- a/nbs/01_basics.ipynb
+++ b/nbs/01_basics.ipynb
@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 1,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -11,7 +11,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 2,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -27,7 +27,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -56,7 +56,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -66,7 +66,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -85,7 +85,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -95,7 +95,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -114,7 +114,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -126,7 +126,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 9,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -154,7 +154,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [
     {
@@ -163,7 +163,7 @@
        "'SomeClass()'"
       ]
      },
-     "execution_count": null,
+     "execution_count": 10,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -182,7 +182,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 11,
    "metadata": {},
    "outputs": [
     {
@@ -191,7 +191,7 @@
        "\"SomeClass(a=1, b='foo')\""
       ]
      },
-     "execution_count": null,
+     "execution_count": 11,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -215,7 +215,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 12,
    "metadata": {},
    "outputs": [
     {
@@ -224,7 +224,7 @@
        "\"AnotherClass(c=SomeClass(a=1, b='foo'), d='bar')\""
       ]
      },
-     "execution_count": null,
+     "execution_count": 12,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -247,7 +247,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 13,
    "metadata": {},
    "outputs": [
     {
@@ -256,7 +256,7 @@
        "\"SomeClass(a=1, b='foo')\""
       ]
      },
-     "execution_count": null,
+     "execution_count": 13,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -271,7 +271,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 14,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -290,7 +290,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 15,
    "metadata": {},
    "outputs": [
     {
@@ -299,7 +299,7 @@
        "\"SomeClass(a=1, b='foo')\""
       ]
      },
-     "execution_count": null,
+     "execution_count": 15,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -313,7 +313,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 16,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -325,7 +325,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 17,
    "metadata": {},
    "outputs": [
     {
@@ -334,7 +334,7 @@
        "(True, False)"
       ]
      },
-     "execution_count": null,
+     "execution_count": 17,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -345,7 +345,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 18,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -375,7 +375,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 19,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -391,7 +391,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 20,
    "metadata": {},
    "outputs": [
     {
@@ -402,7 +402,7 @@
        "        [6, 7, 8]])]"
       ]
      },
-     "execution_count": null,
+     "execution_count": 20,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -414,7 +414,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 21,
    "metadata": {},
    "outputs": [
     {
@@ -423,7 +423,7 @@
        "[array([1, 2])]"
       ]
      },
-     "execution_count": null,
+     "execution_count": 21,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -441,7 +441,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 22,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -458,7 +458,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 23,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -474,7 +474,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 24,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -490,7 +490,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 25,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -500,7 +500,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 26,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -512,7 +512,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 27,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -523,7 +523,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 28,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -536,7 +536,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 29,
    "metadata": {},
    "outputs": [
     {
@@ -552,7 +552,7 @@
        " (None, False)]"
       ]
      },
-     "execution_count": null,
+     "execution_count": 29,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -564,7 +564,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 30,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -581,7 +581,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 31,
    "metadata": {},
    "outputs": [
     {
@@ -590,7 +590,7 @@
        "False"
       ]
      },
-     "execution_count": null,
+     "execution_count": 31,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -601,7 +601,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 32,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -613,7 +613,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 33,
    "metadata": {},
    "outputs": [
     {
@@ -622,7 +622,7 @@
        "False"
       ]
      },
-     "execution_count": null,
+     "execution_count": 33,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -633,7 +633,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 34,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -669,7 +669,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 35,
    "metadata": {},
    "outputs": [
     {
@@ -699,7 +699,7 @@
        "*Dynamically create a class, optionally inheriting from `sup`, containing `fld_names`*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 35,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -710,7 +710,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 36,
    "metadata": {},
    "outputs": [
     {
@@ -719,7 +719,7 @@
        "'_t(a=1, b=3)'"
       ]
      },
-     "execution_count": null,
+     "execution_count": 36,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -749,7 +749,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 37,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -770,7 +770,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 38,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -789,7 +789,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 39,
    "metadata": {},
    "outputs": [
     {
@@ -798,7 +798,7 @@
        "{}"
       ]
      },
-     "execution_count": null,
+     "execution_count": 39,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -817,7 +817,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 40,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -832,7 +832,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 41,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -846,7 +846,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 42,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -859,7 +859,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 43,
    "metadata": {},
    "outputs": [
     {
@@ -887,7 +887,7 @@
        "*Context manager to ignore exceptions*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 43,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -898,7 +898,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 44,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -909,7 +909,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 45,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -923,7 +923,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 46,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -932,7 +932,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 47,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -951,7 +951,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 48,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -970,7 +970,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 49,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -981,7 +981,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 50,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -992,7 +992,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 51,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1018,7 +1018,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 52,
    "metadata": {},
    "outputs": [
     {
@@ -1042,7 +1042,7 @@
        "*Do nothing*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 52,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1053,7 +1053,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 53,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1063,7 +1063,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 54,
    "metadata": {},
    "outputs": [
     {
@@ -1087,7 +1087,7 @@
        "*Do nothing (method)*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 54,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1098,7 +1098,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 55,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1122,7 +1122,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 56,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1141,7 +1141,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 57,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1153,7 +1153,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 58,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1174,7 +1174,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 59,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1200,7 +1200,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 60,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1219,7 +1219,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 61,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1233,7 +1233,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 62,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1243,7 +1243,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 63,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1253,7 +1253,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 64,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1278,7 +1278,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 65,
    "metadata": {},
    "outputs": [
     {
@@ -1287,7 +1287,7 @@
        "(True, False, True, False, 1)"
       ]
      },
-     "execution_count": null,
+     "execution_count": 65,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1305,7 +1305,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 66,
    "metadata": {},
    "outputs": [
     {
@@ -1314,7 +1314,7 @@
        "(True, False, True, False, 1)"
       ]
      },
-     "execution_count": null,
+     "execution_count": 66,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1325,7 +1325,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 67,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1337,7 +1337,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 68,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1347,7 +1347,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 69,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1359,7 +1359,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 70,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1371,7 +1371,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 71,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1383,7 +1383,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 72,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1397,7 +1397,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 73,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1422,7 +1422,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 74,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1445,7 +1445,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 75,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1457,7 +1457,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 76,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1481,7 +1481,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 77,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1500,7 +1500,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 78,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1513,7 +1513,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 79,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1529,7 +1529,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 80,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1555,7 +1555,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 81,
    "metadata": {},
    "outputs": [
     {
@@ -1579,7 +1579,7 @@
        " 'f': {'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}}"
       ]
      },
-     "execution_count": null,
+     "execution_count": 81,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1592,7 +1592,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 82,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1608,7 +1608,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 83,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1620,7 +1620,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 84,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1641,7 +1641,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 85,
    "metadata": {},
    "outputs": [
     {
@@ -1655,7 +1655,7 @@
        "          f={'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]})"
       ]
      },
-     "execution_count": null,
+     "execution_count": 85,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1674,7 +1674,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 86,
    "metadata": {},
    "outputs": [
     {
@@ -1683,7 +1683,7 @@
        "1"
       ]
      },
-     "execution_count": null,
+     "execution_count": 86,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1701,7 +1701,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 87,
    "metadata": {},
    "outputs": [
     {
@@ -1710,7 +1710,7 @@
        "['a', 'b', 'c', 'd', 'e', 'f']"
       ]
      },
-     "execution_count": null,
+     "execution_count": 87,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1721,7 +1721,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 88,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1782,7 +1782,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 89,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1805,7 +1805,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 90,
    "metadata": {},
    "outputs": [
     {
@@ -1814,7 +1814,7 @@
        "__main__._T2a"
       ]
      },
-     "execution_count": null,
+     "execution_count": 90,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1836,7 +1836,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 91,
    "metadata": {},
    "outputs": [
     {
@@ -1845,7 +1845,7 @@
        "typing.Union[__main__._T2a, __main__._T2b]"
       ]
      },
-     "execution_count": null,
+     "execution_count": 91,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1860,7 +1860,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 92,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1885,7 +1885,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 93,
    "metadata": {},
    "outputs": [
     {
@@ -1900,7 +1900,7 @@
        " method_descriptor]"
       ]
      },
-     "execution_count": null,
+     "execution_count": 93,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1918,7 +1918,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 94,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1937,7 +1937,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 95,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1948,7 +1948,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 96,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1972,7 +1972,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 97,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1983,7 +1983,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 98,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1995,7 +1995,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 99,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2015,7 +2015,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 100,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2034,7 +2034,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 101,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2046,7 +2046,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 102,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2072,7 +2072,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 103,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2085,7 +2085,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 104,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2098,7 +2098,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 105,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2111,7 +2111,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 106,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2120,7 +2120,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 107,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2145,7 +2145,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 108,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2165,7 +2165,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 109,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2180,7 +2180,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 110,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2212,7 +2212,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 111,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2229,7 +2229,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 112,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2246,7 +2246,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 113,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2256,7 +2256,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 114,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2273,7 +2273,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 115,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2289,7 +2289,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 116,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2302,7 +2302,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 117,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2325,7 +2325,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 118,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2345,7 +2345,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 119,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2367,7 +2367,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 120,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2380,7 +2380,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 121,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2395,7 +2395,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 122,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2411,7 +2411,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 123,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2433,7 +2433,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 124,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2448,7 +2448,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 125,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2473,7 +2473,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 126,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2494,7 +2494,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 127,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2514,7 +2514,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 128,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2529,7 +2529,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 129,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2541,7 +2541,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 130,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2554,7 +2554,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 131,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2566,7 +2566,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 132,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2581,7 +2581,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 133,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2593,7 +2593,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 134,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2605,7 +2605,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 135,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2614,7 +2614,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 136,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2627,7 +2627,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 137,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2637,7 +2637,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 138,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2649,7 +2649,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 139,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2658,7 +2658,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 140,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2670,7 +2670,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 141,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2692,7 +2692,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 142,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2704,7 +2704,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 143,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2719,7 +2719,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 144,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2731,7 +2731,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 145,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2740,7 +2740,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 146,
    "metadata": {},
    "outputs": [
     {
@@ -2749,7 +2749,7 @@
        "[1, 2]"
       ]
      },
-     "execution_count": null,
+     "execution_count": 146,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2760,7 +2760,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 147,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2772,7 +2772,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 148,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2782,7 +2782,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 149,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2795,7 +2795,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 150,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2808,7 +2808,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 151,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2821,7 +2821,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 152,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2836,7 +2836,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 153,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2853,7 +2853,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 154,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2869,7 +2869,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 155,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2894,7 +2894,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 156,
    "metadata": {},
    "outputs": [
     {
@@ -2922,7 +2922,7 @@
        "*Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 156,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2956,7 +2956,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 157,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2980,7 +2980,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 158,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2999,7 +2999,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 159,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3015,7 +3015,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 160,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3042,7 +3042,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 161,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3067,7 +3067,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 162,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3095,7 +3095,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 163,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3116,7 +3116,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 164,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3139,7 +3139,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 165,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3165,7 +3165,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 166,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3193,7 +3193,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 167,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3216,7 +3216,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 168,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3226,7 +3226,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 169,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3261,7 +3261,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 170,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3283,7 +3283,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 171,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3296,7 +3296,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 172,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3319,7 +3319,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 173,
    "metadata": {},
    "outputs": [
     {
@@ -3354,7 +3354,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 174,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3371,7 +3371,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 175,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3382,7 +3382,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 176,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3396,7 +3396,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 177,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3408,7 +3408,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 178,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3419,7 +3419,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 179,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3434,7 +3434,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 180,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3446,7 +3446,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 181,
    "metadata": {},
    "outputs": [
     {
@@ -3455,7 +3455,7 @@
        "[0, 1, 2, 3, 4, 5]"
       ]
      },
-     "execution_count": null,
+     "execution_count": 181,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3466,7 +3466,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 182,
    "metadata": {},
    "outputs": [
     {
@@ -3475,7 +3475,7 @@
        "['abc', 'xyz', 'foo', 'bar']"
       ]
      },
-     "execution_count": null,
+     "execution_count": 182,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3486,7 +3486,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 183,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3498,7 +3498,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 184,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3508,7 +3508,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 185,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3520,7 +3520,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 186,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3532,7 +3532,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 187,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3544,7 +3544,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 188,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3555,7 +3555,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 189,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3567,7 +3567,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 190,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3581,7 +3581,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 191,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3593,7 +3593,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 192,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3604,7 +3604,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 193,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3616,7 +3616,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 194,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3625,7 +3625,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 195,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3647,7 +3647,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 196,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3663,7 +3663,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 197,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3679,7 +3679,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 198,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3695,7 +3695,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 199,
    "metadata": {},
    "outputs": [
     {
@@ -3704,7 +3704,7 @@
        "{1: [0], 3: [0, 2], 7: [0], 5: [3, 7], 8: [4], 4: [5]}"
       ]
      },
-     "execution_count": null,
+     "execution_count": 199,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3716,7 +3716,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 200,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3729,7 +3729,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 201,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3739,7 +3739,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 202,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3751,7 +3751,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 203,
    "metadata": {},
    "outputs": [
     {
@@ -3760,7 +3760,7 @@
        "{65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G', 72: 'H'}"
       ]
      },
-     "execution_count": null,
+     "execution_count": 203,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3772,7 +3772,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 204,
    "metadata": {},
    "outputs": [
     {
@@ -3781,7 +3781,7 @@
        "{65: 'A', 66: 'B', 70: 'F', 71: 'G'}"
       ]
      },
-     "execution_count": null,
+     "execution_count": 204,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3792,7 +3792,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 205,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3804,7 +3804,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 206,
    "metadata": {},
    "outputs": [
     {
@@ -3813,7 +3813,7 @@
        "{65: 'A', 66: 'B'}"
       ]
      },
-     "execution_count": null,
+     "execution_count": 206,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3824,7 +3824,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 207,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3836,7 +3836,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 208,
    "metadata": {},
    "outputs": [
     {
@@ -3845,7 +3845,7 @@
        "{70: 'F', 71: 'G'}"
       ]
      },
-     "execution_count": null,
+     "execution_count": 208,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3856,7 +3856,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 209,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3869,7 +3869,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 210,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3881,7 +3881,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 211,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3893,7 +3893,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 212,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3902,7 +3902,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 213,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3928,7 +3928,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 214,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3947,7 +3947,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 215,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3964,7 +3964,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 216,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3980,7 +3980,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 217,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3996,7 +3996,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 218,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4013,7 +4013,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 219,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4022,7 +4022,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 220,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4035,7 +4035,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 221,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4047,7 +4047,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 222,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4061,7 +4061,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 223,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4078,7 +4078,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 224,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4091,7 +4091,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 225,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4101,7 +4101,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 226,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4113,7 +4113,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 227,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4122,7 +4122,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 228,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4136,7 +4136,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 229,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4147,7 +4147,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 230,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4162,7 +4162,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 231,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4173,7 +4173,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 232,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4190,7 +4190,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 233,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4202,7 +4202,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 234,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4220,7 +4220,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 235,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4248,7 +4248,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 236,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4259,7 +4259,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 237,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4271,7 +4271,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 238,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4288,7 +4288,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 239,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4302,7 +4302,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 240,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4314,7 +4314,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 241,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4326,7 +4326,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 242,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4338,7 +4338,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 243,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4349,7 +4349,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 244,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4374,7 +4374,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 245,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4389,7 +4389,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 246,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4405,7 +4405,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 247,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4421,7 +4421,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 248,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4434,7 +4434,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 249,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4444,7 +4444,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 250,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4456,7 +4456,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 251,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4465,7 +4465,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 252,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4480,7 +4480,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 253,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4498,7 +4498,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 254,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4519,7 +4519,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 255,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4528,7 +4528,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 256,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4540,7 +4540,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 257,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4549,7 +4549,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 258,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4561,7 +4561,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 259,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4570,7 +4570,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 260,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4582,7 +4582,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 261,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4592,7 +4592,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 262,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4604,7 +4604,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 263,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4627,7 +4627,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 264,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4648,7 +4648,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 265,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4692,7 +4692,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 266,
    "metadata": {},
    "outputs": [
     {
@@ -4720,7 +4720,7 @@
        "*A `tuple` with elementwise ops and more friendly __init__ behavior*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 266,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -4759,7 +4759,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 267,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4777,7 +4777,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 268,
    "metadata": {},
    "outputs": [
     {
@@ -4805,7 +4805,7 @@
        "*`+` is already defined in `tuple` for concat, so use `add` instead*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 268,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -4816,7 +4816,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 269,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4827,7 +4827,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 270,
    "metadata": {},
    "outputs": [
     {
@@ -4855,7 +4855,7 @@
        "*`*` is already defined in `tuple` for replicating, so use `mul` instead*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 270,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -4866,7 +4866,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 271,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4893,7 +4893,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 272,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4912,7 +4912,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 273,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4924,7 +4924,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 274,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4950,7 +4950,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 275,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4966,7 +4966,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 276,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4988,7 +4988,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 277,
    "metadata": {},
    "outputs": [
     {
@@ -5016,7 +5016,7 @@
        "*Same as `partial`, except you can use `arg0` `arg1` etc param placeholders*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 277,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -5036,7 +5036,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 278,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5056,7 +5056,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 279,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5077,7 +5077,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 280,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5093,7 +5093,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 281,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5109,7 +5109,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 282,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5118,7 +5118,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 283,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5130,7 +5130,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 284,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5140,7 +5140,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 285,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5157,7 +5157,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 286,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5173,7 +5173,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 287,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5189,7 +5189,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 288,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5205,7 +5205,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 289,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5215,7 +5215,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 290,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5234,7 +5234,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 291,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5250,7 +5250,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 292,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5264,7 +5264,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 293,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5275,7 +5275,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 294,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5291,7 +5291,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 295,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5311,7 +5311,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 296,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5327,7 +5327,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 297,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5338,7 +5338,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 298,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5350,7 +5350,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 299,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5360,7 +5360,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 300,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5370,7 +5370,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 301,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5382,7 +5382,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 302,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5407,7 +5407,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 303,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5446,7 +5446,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 304,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5461,7 +5461,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 305,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5480,7 +5480,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 306,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5521,7 +5521,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 307,
    "metadata": {},
    "outputs": [
     {
@@ -5530,7 +5530,7 @@
        "[5, 2]"
       ]
      },
-     "execution_count": null,
+     "execution_count": 307,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -5551,7 +5551,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 308,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5576,7 +5576,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 309,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5585,7 +5585,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 310,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5608,7 +5608,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 311,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5618,7 +5618,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 312,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5628,7 +5628,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 313,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5640,7 +5640,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 314,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5657,12 +5657,7 @@
     "            nf.__qualname__ = f\"{c_.__name__}.{nm}\"\n",
     "            if cls_method: setattr(c_, nm, _clsmethod(nf))\n",
     "            else:\n",
-    "                if set_prop:\n",
-    "                    if isinstance(getattr(c_, nm, None), property):\n",
-    "                        prop = getattr(c_, nm)\n",
-    "                        setattr(c_, nm, prop.setter(nf))\n",
-    "                    else:\n",
-    "                        raise AttributeError(f\"Cannot set property setter: {nm} is not a property of {c_}\")\n",
+    "                if set_prop: setattr(c_, nm, getattr(c_, nm).setter(nf))\n",
     "                elif as_prop: setattr(c_, nm, property(nf))\n",
     "                else:\n",
     "                    onm = '_orig_'+nm\n",
@@ -5682,7 +5677,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 315,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5704,7 +5699,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 316,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5727,7 +5722,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 317,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5748,7 +5743,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 318,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5768,7 +5763,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 319,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5797,7 +5792,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 320,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5815,7 +5810,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 321,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5837,7 +5832,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 322,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5860,7 +5855,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 323,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5887,7 +5882,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 324,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5900,7 +5895,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 325,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5922,7 +5917,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 326,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5936,7 +5931,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 327,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5957,7 +5952,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 328,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5984,7 +5979,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 329,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5996,7 +5991,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 330,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6006,7 +6001,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 331,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6021,7 +6016,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 332,
    "metadata": {},
    "outputs": [
     {
@@ -6029,7 +6024,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1079){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1080){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ImportEnum\n",
        "\n",
@@ -6041,7 +6036,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1079){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1080){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ImportEnum\n",
        "\n",
@@ -6051,7 +6046,7 @@
        "*An `Enum` that can have its values imported*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 332,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6062,7 +6057,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 333,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6074,7 +6069,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 334,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6086,7 +6081,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 335,
    "metadata": {},
    "outputs": [
     {
@@ -6094,7 +6089,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1087){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1088){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### StrEnum\n",
        "\n",
@@ -6106,7 +6101,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1087){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1088){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### StrEnum\n",
        "\n",
@@ -6116,7 +6111,7 @@
        "*An `ImportEnum` that behaves like a `str`*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 335,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6127,7 +6122,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 336,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6139,7 +6134,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 337,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6151,7 +6146,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 338,
    "metadata": {},
    "outputs": [
     {
@@ -6159,7 +6154,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1097){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1098){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ValEnum\n",
        "\n",
@@ -6171,7 +6166,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1097){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1098){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ValEnum\n",
        "\n",
@@ -6181,7 +6176,7 @@
        "*An `ImportEnum` that stringifies using values*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 338,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6192,7 +6187,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 339,
    "metadata": {},
    "outputs": [
     {
@@ -6213,7 +6208,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 340,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6240,7 +6235,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 341,
    "metadata": {},
    "outputs": [
     {
@@ -6248,7 +6243,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1102){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1103){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### Stateful\n",
        "\n",
@@ -6259,7 +6254,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1102){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1103){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### Stateful\n",
        "\n",
@@ -6268,7 +6263,7 @@
        "*A base class/mixin for objects that should not serialize all their state*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 341,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6279,7 +6274,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 342,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6306,7 +6301,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 343,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6329,7 +6324,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 344,
    "metadata": {},
    "outputs": [
     {
@@ -6353,7 +6348,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 345,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6377,7 +6372,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 346,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6390,7 +6385,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 347,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6402,7 +6397,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 348,
    "metadata": {},
    "outputs": [
     {
@@ -6410,7 +6405,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1140){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PrettyString\n",
        "\n",
@@ -6421,7 +6416,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1139){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1140){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PrettyString\n",
        "\n",
@@ -6430,7 +6425,7 @@
        "*Little hack to get strings to show properly in Jupyter.*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 348,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6448,7 +6443,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 349,
    "metadata": {},
    "outputs": [
     {
@@ -6457,7 +6452,7 @@
        "'a string\\nwith\\nnew\\nlines and\\ttabs'"
       ]
      },
-     "execution_count": null,
+     "execution_count": 349,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6476,7 +6471,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 350,
    "metadata": {},
    "outputs": [
     {
@@ -6488,7 +6483,7 @@
        "lines and\ttabs"
       ]
      },
-     "execution_count": null,
+     "execution_count": 350,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6499,7 +6494,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 351,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6514,7 +6509,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 352,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6525,7 +6520,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 353,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6540,7 +6535,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 354,
    "metadata": {},
    "outputs": [
     {
@@ -6549,7 +6544,7 @@
        "22"
       ]
      },
-     "execution_count": null,
+     "execution_count": 354,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6560,7 +6555,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 355,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6573,7 +6568,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 356,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6586,7 +6581,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 357,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6606,7 +6601,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 358,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6616,7 +6611,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 359,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6640,7 +6635,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 360,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6652,7 +6647,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 361,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6668,7 +6663,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 362,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6687,7 +6682,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 363,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6701,7 +6696,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 364,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6717,7 +6712,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 365,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6732,7 +6727,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 366,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6745,7 +6740,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 367,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6757,7 +6752,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 368,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6775,7 +6770,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 369,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6793,7 +6788,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 370,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6844,7 +6839,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 371,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6864,7 +6859,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 372,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6886,7 +6881,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 373,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6912,7 +6907,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 374,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6925,7 +6920,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 375,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6958,7 +6953,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 376,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6981,7 +6976,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 377,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6994,7 +6989,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 378,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7011,7 +7006,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 379,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7026,7 +7021,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 380,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7036,7 +7031,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 381,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7056,7 +7051,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 382,
    "metadata": {},
    "outputs": [
     {
@@ -7080,7 +7075,7 @@
        "*Same as `get_ipython` but returns `False` if not in IPython*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 382,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7091,7 +7086,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 383,
    "metadata": {},
    "outputs": [
     {
@@ -7115,7 +7110,7 @@
        "*Check if code is running in some kind of IPython environment*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 383,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7126,7 +7121,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 384,
    "metadata": {},
    "outputs": [
     {
@@ -7150,7 +7145,7 @@
        "*Check if the code is running in Google Colaboratory*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 384,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7161,7 +7156,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 385,
    "metadata": {},
    "outputs": [
     {
@@ -7185,7 +7180,7 @@
        "*Check if the code is running in a jupyter notebook*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 385,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7196,7 +7191,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 386,
    "metadata": {},
    "outputs": [
     {
@@ -7220,7 +7215,7 @@
        "*Check if the code is running in a jupyter notebook*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 386,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7238,7 +7233,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 387,
    "metadata": {},
    "outputs": [
     {
@@ -7247,7 +7242,7 @@
        "(True, True, False, True)"
       ]
      },
-     "execution_count": null,
+     "execution_count": 387,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7265,7 +7260,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 388,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7286,9 +7281,21 @@
    "split_at_heading": true
   },
   "kernelspec": {
-   "display_name": "python3",
+   "display_name": "Python 3 (ipykernel)",
    "language": "python",
    "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.11"
   }
  },
  "nbformat": 4,

From 3416b22f96782b70560a618f579d75d745f19fe0 Mon Sep 17 00:00:00 2001
From: Jay Suh 
Date: Thu, 27 Mar 2025 17:16:38 -0500
Subject: [PATCH 067/182] Clean notebooks

---
 nbs/01_basics.ipynb | 898 ++++++++++++++++++++++----------------------
 1 file changed, 443 insertions(+), 455 deletions(-)

diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb
index 55c0ba2f..a145f007 100644
--- a/nbs/01_basics.ipynb
+++ b/nbs/01_basics.ipynb
@@ -2,7 +2,7 @@
  "cells": [
   {
    "cell_type": "code",
-   "execution_count": 1,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -11,7 +11,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 2,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -27,7 +27,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 3,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -56,7 +56,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -66,7 +66,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -85,7 +85,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -95,7 +95,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -114,7 +114,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -126,7 +126,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -154,7 +154,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -163,7 +163,7 @@
        "'SomeClass()'"
       ]
      },
-     "execution_count": 10,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -182,7 +182,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -191,7 +191,7 @@
        "\"SomeClass(a=1, b='foo')\""
       ]
      },
-     "execution_count": 11,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -215,7 +215,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -224,7 +224,7 @@
        "\"AnotherClass(c=SomeClass(a=1, b='foo'), d='bar')\""
       ]
      },
-     "execution_count": 12,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -247,7 +247,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -256,7 +256,7 @@
        "\"SomeClass(a=1, b='foo')\""
       ]
      },
-     "execution_count": 13,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -271,7 +271,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -290,7 +290,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -299,7 +299,7 @@
        "\"SomeClass(a=1, b='foo')\""
       ]
      },
-     "execution_count": 15,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -313,7 +313,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -325,7 +325,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -334,7 +334,7 @@
        "(True, False)"
       ]
      },
-     "execution_count": 17,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -345,7 +345,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -375,7 +375,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 19,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -391,7 +391,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -402,7 +402,7 @@
        "        [6, 7, 8]])]"
       ]
      },
-     "execution_count": 20,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -414,7 +414,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -423,7 +423,7 @@
        "[array([1, 2])]"
       ]
      },
-     "execution_count": 21,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -441,7 +441,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -458,7 +458,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 23,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -474,7 +474,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 24,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -490,7 +490,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 25,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -500,7 +500,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 26,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -512,7 +512,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 27,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -523,7 +523,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 28,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -536,7 +536,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 29,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -552,7 +552,7 @@
        " (None, False)]"
       ]
      },
-     "execution_count": 29,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -564,7 +564,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 30,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -581,7 +581,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 31,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -590,7 +590,7 @@
        "False"
       ]
      },
-     "execution_count": 31,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -601,7 +601,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 32,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -613,7 +613,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 33,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -622,7 +622,7 @@
        "False"
       ]
      },
-     "execution_count": 33,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -633,7 +633,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 34,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -669,7 +669,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 35,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -699,7 +699,7 @@
        "*Dynamically create a class, optionally inheriting from `sup`, containing `fld_names`*"
       ]
      },
-     "execution_count": 35,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -710,7 +710,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 36,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -719,7 +719,7 @@
        "'_t(a=1, b=3)'"
       ]
      },
-     "execution_count": 36,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -749,7 +749,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 37,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -770,7 +770,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 38,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -789,7 +789,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 39,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -798,7 +798,7 @@
        "{}"
       ]
      },
-     "execution_count": 39,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -817,7 +817,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 40,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -832,7 +832,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 41,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -846,7 +846,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 42,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -859,7 +859,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 43,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -887,7 +887,7 @@
        "*Context manager to ignore exceptions*"
       ]
      },
-     "execution_count": 43,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -898,7 +898,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 44,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -909,7 +909,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 45,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -923,7 +923,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 46,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -932,7 +932,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 47,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -951,7 +951,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 48,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -970,7 +970,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 49,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -981,7 +981,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 50,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -992,7 +992,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 51,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1018,7 +1018,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 52,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1042,7 +1042,7 @@
        "*Do nothing*"
       ]
      },
-     "execution_count": 52,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1053,7 +1053,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 53,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1063,7 +1063,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 54,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1087,7 +1087,7 @@
        "*Do nothing (method)*"
       ]
      },
-     "execution_count": 54,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1098,7 +1098,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 55,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1122,7 +1122,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 56,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1141,7 +1141,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 57,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1153,7 +1153,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 58,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1174,7 +1174,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 59,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1200,7 +1200,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 60,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1219,7 +1219,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 61,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1233,7 +1233,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 62,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1243,7 +1243,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 63,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1253,7 +1253,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 64,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1278,7 +1278,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 65,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1287,7 +1287,7 @@
        "(True, False, True, False, 1)"
       ]
      },
-     "execution_count": 65,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1305,7 +1305,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 66,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1314,7 +1314,7 @@
        "(True, False, True, False, 1)"
       ]
      },
-     "execution_count": 66,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1325,7 +1325,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 67,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1337,7 +1337,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 68,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1347,7 +1347,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 69,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1359,7 +1359,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 70,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1371,7 +1371,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 71,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1383,7 +1383,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 72,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1397,7 +1397,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 73,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1422,7 +1422,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 74,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1445,7 +1445,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 75,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1457,7 +1457,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 76,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1481,7 +1481,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 77,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1500,7 +1500,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 78,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1513,7 +1513,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 79,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1529,7 +1529,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 80,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1555,7 +1555,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 81,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1579,7 +1579,7 @@
        " 'f': {'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]}}"
       ]
      },
-     "execution_count": 81,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1592,7 +1592,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 82,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1608,7 +1608,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 83,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1620,7 +1620,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 84,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1641,7 +1641,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 85,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1655,7 +1655,7 @@
        "          f={'c': 1, 'd': 2, 'e': 4, 'f': [1, 2, 3, 4, 5]})"
       ]
      },
-     "execution_count": 85,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1674,7 +1674,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 86,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1683,7 +1683,7 @@
        "1"
       ]
      },
-     "execution_count": 86,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1701,7 +1701,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 87,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1710,7 +1710,7 @@
        "['a', 'b', 'c', 'd', 'e', 'f']"
       ]
      },
-     "execution_count": 87,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1721,7 +1721,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 88,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1782,7 +1782,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 89,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1805,7 +1805,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 90,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1814,7 +1814,7 @@
        "__main__._T2a"
       ]
      },
-     "execution_count": 90,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1836,7 +1836,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 91,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1845,7 +1845,7 @@
        "typing.Union[__main__._T2a, __main__._T2b]"
       ]
      },
-     "execution_count": 91,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1860,7 +1860,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 92,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1885,7 +1885,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 93,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -1900,7 +1900,7 @@
        " method_descriptor]"
       ]
      },
-     "execution_count": 93,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1918,7 +1918,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 94,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1937,7 +1937,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 95,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1948,7 +1948,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 96,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1972,7 +1972,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 97,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1983,7 +1983,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 98,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1995,7 +1995,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 99,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2015,7 +2015,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 100,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2034,7 +2034,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 101,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2046,7 +2046,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 102,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2072,7 +2072,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 103,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2085,7 +2085,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 104,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2098,7 +2098,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 105,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2111,7 +2111,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 106,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2120,7 +2120,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 107,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2145,7 +2145,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 108,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2165,7 +2165,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 109,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2180,7 +2180,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 110,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2212,7 +2212,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 111,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2229,7 +2229,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 112,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2246,7 +2246,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 113,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2256,7 +2256,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 114,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2273,7 +2273,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 115,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2289,7 +2289,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 116,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2302,7 +2302,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 117,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2325,7 +2325,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 118,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2345,7 +2345,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 119,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2367,7 +2367,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 120,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2380,7 +2380,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 121,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2395,7 +2395,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 122,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2411,7 +2411,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 123,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2433,7 +2433,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 124,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2448,7 +2448,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 125,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2473,7 +2473,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 126,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2494,7 +2494,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 127,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2514,7 +2514,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 128,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2529,7 +2529,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 129,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2541,7 +2541,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 130,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2554,7 +2554,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 131,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2566,7 +2566,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 132,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2581,7 +2581,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 133,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2593,7 +2593,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 134,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2605,7 +2605,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 135,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2614,7 +2614,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 136,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2627,7 +2627,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 137,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2637,7 +2637,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 138,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2649,7 +2649,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 139,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2658,7 +2658,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 140,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2670,7 +2670,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 141,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2692,7 +2692,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 142,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2704,7 +2704,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 143,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2719,7 +2719,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 144,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2731,7 +2731,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 145,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2740,7 +2740,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 146,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -2749,7 +2749,7 @@
        "[1, 2]"
       ]
      },
-     "execution_count": 146,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2760,7 +2760,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 147,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2772,7 +2772,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 148,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2782,7 +2782,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 149,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2795,7 +2795,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 150,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2808,7 +2808,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 151,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2821,7 +2821,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 152,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2836,7 +2836,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 153,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2853,7 +2853,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 154,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2869,7 +2869,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 155,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2894,7 +2894,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 156,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -2922,7 +2922,7 @@
        "*Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`*"
       ]
      },
-     "execution_count": 156,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2956,7 +2956,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 157,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2980,7 +2980,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 158,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -2999,7 +2999,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 159,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3015,7 +3015,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 160,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3042,7 +3042,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 161,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3067,7 +3067,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 162,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3095,7 +3095,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 163,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3116,7 +3116,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 164,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3139,7 +3139,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 165,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3165,7 +3165,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 166,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3193,7 +3193,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 167,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3216,7 +3216,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 168,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3226,7 +3226,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 169,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3261,7 +3261,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 170,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3283,7 +3283,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 171,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3296,7 +3296,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 172,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3319,7 +3319,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 173,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -3354,7 +3354,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 174,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3371,7 +3371,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 175,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3382,7 +3382,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 176,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3396,7 +3396,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 177,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3408,7 +3408,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 178,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3419,7 +3419,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 179,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3434,7 +3434,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 180,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3446,7 +3446,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 181,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -3455,7 +3455,7 @@
        "[0, 1, 2, 3, 4, 5]"
       ]
      },
-     "execution_count": 181,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3466,7 +3466,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 182,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -3475,7 +3475,7 @@
        "['abc', 'xyz', 'foo', 'bar']"
       ]
      },
-     "execution_count": 182,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3486,7 +3486,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 183,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3498,7 +3498,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 184,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3508,7 +3508,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 185,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3520,7 +3520,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 186,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3532,7 +3532,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 187,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3544,7 +3544,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 188,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3555,7 +3555,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 189,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3567,7 +3567,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 190,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3581,7 +3581,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 191,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3593,7 +3593,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 192,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3604,7 +3604,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 193,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3616,7 +3616,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 194,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3625,7 +3625,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 195,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3647,7 +3647,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 196,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3663,7 +3663,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 197,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3679,7 +3679,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 198,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3695,7 +3695,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 199,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -3704,7 +3704,7 @@
        "{1: [0], 3: [0, 2], 7: [0], 5: [3, 7], 8: [4], 4: [5]}"
       ]
      },
-     "execution_count": 199,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3716,7 +3716,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 200,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3729,7 +3729,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 201,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3739,7 +3739,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 202,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3751,7 +3751,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 203,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -3760,7 +3760,7 @@
        "{65: 'A', 66: 'B', 67: 'C', 68: 'D', 69: 'E', 70: 'F', 71: 'G', 72: 'H'}"
       ]
      },
-     "execution_count": 203,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3772,7 +3772,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 204,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -3781,7 +3781,7 @@
        "{65: 'A', 66: 'B', 70: 'F', 71: 'G'}"
       ]
      },
-     "execution_count": 204,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3792,7 +3792,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 205,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3804,7 +3804,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 206,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -3813,7 +3813,7 @@
        "{65: 'A', 66: 'B'}"
       ]
      },
-     "execution_count": 206,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3824,7 +3824,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 207,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3836,7 +3836,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 208,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -3845,7 +3845,7 @@
        "{70: 'F', 71: 'G'}"
       ]
      },
-     "execution_count": 208,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3856,7 +3856,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 209,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3869,7 +3869,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 210,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3881,7 +3881,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 211,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3893,7 +3893,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 212,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3902,7 +3902,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 213,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3928,7 +3928,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 214,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3947,7 +3947,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 215,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3964,7 +3964,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 216,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3980,7 +3980,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 217,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -3996,7 +3996,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 218,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4013,7 +4013,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 219,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4022,7 +4022,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 220,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4035,7 +4035,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 221,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4047,7 +4047,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 222,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4061,7 +4061,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 223,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4078,7 +4078,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 224,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4091,7 +4091,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 225,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4101,7 +4101,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 226,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4113,7 +4113,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 227,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4122,7 +4122,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 228,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4136,7 +4136,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 229,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4147,7 +4147,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 230,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4162,7 +4162,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 231,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4173,7 +4173,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 232,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4190,7 +4190,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 233,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4202,7 +4202,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 234,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4220,7 +4220,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 235,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4248,7 +4248,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 236,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4259,7 +4259,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 237,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4271,7 +4271,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 238,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4288,7 +4288,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 239,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4302,7 +4302,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 240,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4314,7 +4314,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 241,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4326,7 +4326,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 242,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4338,7 +4338,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 243,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4349,7 +4349,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 244,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4374,7 +4374,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 245,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4389,7 +4389,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 246,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4405,7 +4405,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 247,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4421,7 +4421,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 248,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4434,7 +4434,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 249,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4444,7 +4444,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 250,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4456,7 +4456,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 251,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4465,7 +4465,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 252,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4480,7 +4480,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 253,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4498,7 +4498,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 254,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4519,7 +4519,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 255,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4528,7 +4528,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 256,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4540,7 +4540,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 257,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4549,7 +4549,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 258,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4561,7 +4561,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 259,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4570,7 +4570,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 260,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4582,7 +4582,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 261,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4592,7 +4592,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 262,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4604,7 +4604,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 263,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4627,7 +4627,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 264,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4648,7 +4648,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 265,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4692,7 +4692,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 266,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -4720,7 +4720,7 @@
        "*A `tuple` with elementwise ops and more friendly __init__ behavior*"
       ]
      },
-     "execution_count": 266,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -4759,7 +4759,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 267,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4777,7 +4777,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 268,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -4805,7 +4805,7 @@
        "*`+` is already defined in `tuple` for concat, so use `add` instead*"
       ]
      },
-     "execution_count": 268,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -4816,7 +4816,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 269,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4827,7 +4827,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 270,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -4855,7 +4855,7 @@
        "*`*` is already defined in `tuple` for replicating, so use `mul` instead*"
       ]
      },
-     "execution_count": 270,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -4866,7 +4866,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 271,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4893,7 +4893,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 272,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4912,7 +4912,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 273,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4924,7 +4924,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 274,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4950,7 +4950,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 275,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4966,7 +4966,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 276,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -4988,7 +4988,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 277,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -5016,7 +5016,7 @@
        "*Same as `partial`, except you can use `arg0` `arg1` etc param placeholders*"
       ]
      },
-     "execution_count": 277,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -5036,7 +5036,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 278,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5056,7 +5056,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 279,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5077,7 +5077,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 280,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5093,7 +5093,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 281,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5109,7 +5109,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 282,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5118,7 +5118,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 283,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5130,7 +5130,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 284,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5140,7 +5140,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 285,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5157,7 +5157,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 286,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5173,7 +5173,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 287,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5189,7 +5189,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 288,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5205,7 +5205,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 289,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5215,7 +5215,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 290,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5234,7 +5234,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 291,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5250,7 +5250,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 292,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5264,7 +5264,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 293,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5275,7 +5275,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 294,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5291,7 +5291,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 295,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5311,7 +5311,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 296,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5327,7 +5327,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 297,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5338,7 +5338,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 298,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5350,7 +5350,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 299,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5360,7 +5360,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 300,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5370,7 +5370,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 301,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5382,7 +5382,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 302,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5407,7 +5407,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 303,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5446,7 +5446,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 304,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5461,7 +5461,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 305,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5480,7 +5480,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 306,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5521,7 +5521,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 307,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -5530,7 +5530,7 @@
        "[5, 2]"
       ]
      },
-     "execution_count": 307,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -5551,7 +5551,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 308,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5576,7 +5576,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 309,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5585,7 +5585,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 310,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5608,7 +5608,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 311,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5618,7 +5618,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 312,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5628,7 +5628,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 313,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5640,7 +5640,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 314,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5677,7 +5677,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 315,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5699,7 +5699,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 316,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5722,7 +5722,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 317,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5743,7 +5743,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 318,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5763,7 +5763,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 319,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5792,7 +5792,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 320,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5810,7 +5810,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 321,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5832,7 +5832,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 322,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5855,7 +5855,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 323,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5882,7 +5882,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 324,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5895,7 +5895,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 325,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5917,7 +5917,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 326,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5931,7 +5931,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 327,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5952,7 +5952,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 328,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5979,7 +5979,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 329,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -5991,7 +5991,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 330,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6001,7 +6001,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 331,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6016,7 +6016,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 332,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6046,7 +6046,7 @@
        "*An `Enum` that can have its values imported*"
       ]
      },
-     "execution_count": 332,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6057,7 +6057,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 333,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6069,7 +6069,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 334,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6081,7 +6081,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 335,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6111,7 +6111,7 @@
        "*An `ImportEnum` that behaves like a `str`*"
       ]
      },
-     "execution_count": 335,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6122,7 +6122,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 336,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6134,7 +6134,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 337,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6146,7 +6146,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 338,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6176,7 +6176,7 @@
        "*An `ImportEnum` that stringifies using values*"
       ]
      },
-     "execution_count": 338,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6187,7 +6187,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 339,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6208,7 +6208,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 340,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6235,7 +6235,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 341,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6263,7 +6263,7 @@
        "*A base class/mixin for objects that should not serialize all their state*"
       ]
      },
-     "execution_count": 341,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6274,7 +6274,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 342,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6301,7 +6301,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 343,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6324,7 +6324,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 344,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6348,7 +6348,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 345,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6372,7 +6372,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 346,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6385,7 +6385,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 347,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6397,7 +6397,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 348,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6425,7 +6425,7 @@
        "*Little hack to get strings to show properly in Jupyter.*"
       ]
      },
-     "execution_count": 348,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6443,7 +6443,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 349,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6452,7 +6452,7 @@
        "'a string\\nwith\\nnew\\nlines and\\ttabs'"
       ]
      },
-     "execution_count": 349,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6471,7 +6471,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 350,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6483,7 +6483,7 @@
        "lines and\ttabs"
       ]
      },
-     "execution_count": 350,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6494,7 +6494,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 351,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6509,7 +6509,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 352,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6520,7 +6520,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 353,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6535,7 +6535,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 354,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -6544,7 +6544,7 @@
        "22"
       ]
      },
-     "execution_count": 354,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -6555,7 +6555,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 355,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6568,7 +6568,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 356,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6581,7 +6581,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 357,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6601,7 +6601,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 358,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6611,7 +6611,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 359,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6635,7 +6635,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 360,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6647,7 +6647,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 361,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6663,7 +6663,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 362,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6682,7 +6682,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 363,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6696,7 +6696,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 364,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6712,7 +6712,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 365,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6727,7 +6727,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 366,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6740,7 +6740,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 367,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6752,7 +6752,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 368,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6770,7 +6770,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 369,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6788,7 +6788,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 370,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6839,7 +6839,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 371,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6859,7 +6859,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 372,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6881,7 +6881,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 373,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6907,7 +6907,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 374,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6920,7 +6920,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 375,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6953,7 +6953,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 376,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6976,7 +6976,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 377,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -6989,7 +6989,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 378,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7006,7 +7006,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 379,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7021,7 +7021,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 380,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7031,7 +7031,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 381,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7051,7 +7051,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 382,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -7075,7 +7075,7 @@
        "*Same as `get_ipython` but returns `False` if not in IPython*"
       ]
      },
-     "execution_count": 382,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7086,7 +7086,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 383,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -7110,7 +7110,7 @@
        "*Check if code is running in some kind of IPython environment*"
       ]
      },
-     "execution_count": 383,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7121,7 +7121,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 384,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -7145,7 +7145,7 @@
        "*Check if the code is running in Google Colaboratory*"
       ]
      },
-     "execution_count": 384,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7156,7 +7156,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 385,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -7180,7 +7180,7 @@
        "*Check if the code is running in a jupyter notebook*"
       ]
      },
-     "execution_count": 385,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7191,7 +7191,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 386,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -7215,7 +7215,7 @@
        "*Check if the code is running in a jupyter notebook*"
       ]
      },
-     "execution_count": 386,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7233,7 +7233,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 387,
+   "execution_count": null,
    "metadata": {},
    "outputs": [
     {
@@ -7242,7 +7242,7 @@
        "(True, True, False, True)"
       ]
      },
-     "execution_count": 387,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -7260,7 +7260,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 388,
+   "execution_count": null,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -7281,21 +7281,9 @@
    "split_at_heading": true
   },
   "kernelspec": {
-   "display_name": "Python 3 (ipykernel)",
+   "display_name": "python3",
    "language": "python",
    "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.11.11"
   }
  },
  "nbformat": 4,

From 52209142e14f97e7a8ff927acff76d547e92c967 Mon Sep 17 00:00:00 2001
From: Carlo Lepelaars 
Date: Sat, 29 Mar 2025 13:02:48 +0100
Subject: [PATCH 068/182] Add headers arg to urljson

---
 fastcore/net.py   |  4 ++--
 nbs/03b_net.ipynb | 22 ++++++++--------------
 2 files changed, 10 insertions(+), 16 deletions(-)

diff --git a/fastcore/net.py b/fastcore/net.py
index 2f26f818..4effd30a 100644
--- a/fastcore/net.py
+++ b/fastcore/net.py
@@ -127,9 +127,9 @@ def urlread(url, data=None, headers=None, decode=True, return_json=False, return
     return (res,dict(hdrs)) if return_headers else res
 
 # %% ../nbs/03b_net.ipynb
-def urljson(url, data=None, timeout=None):
+def urljson(url, data=None, timeout=None, headers=None):
     "Retrieve `url` and decode json"
-    res = urlread(url, data=data, timeout=timeout)
+    res = urlread(url, data=data, timeout=timeout, headers=headers)
     return json.loads(res) if res else {}
 
 # %% ../nbs/03b_net.ipynb
diff --git a/nbs/03b_net.ipynb b/nbs/03b_net.ipynb
index 7131fb5e..c4cfb3fd 100644
--- a/nbs/03b_net.ipynb
+++ b/nbs/03b_net.ipynb
@@ -191,7 +191,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/net.py#L64){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/net.py#L67){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### HTTP4xxClientError\n",
        "\n",
@@ -202,7 +202,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/net.py#L64){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/net.py#L67){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### HTTP4xxClientError\n",
        "\n",
@@ -230,7 +230,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/net.py#L69){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/net.py#L72){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### HTTP5xxServerError\n",
        "\n",
@@ -241,7 +241,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/fastai/fastcore/blob/master/fastcore/net.py#L69){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/net.py#L72){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### HTTP5xxServerError\n",
        "\n",
@@ -347,7 +347,8 @@
       "====Error Body====\n",
       "{\n",
       "  \"message\": \"Not Found\",\n",
-      "  \"documentation_url\": \"https://docs.github.com/rest\"\n",
+      "  \"documentation_url\": \"https://docs.github.com/rest\",\n",
+      "  \"status\": \"404\"\n",
       "}\n",
       "\n"
      ]
@@ -387,9 +388,9 @@
    "outputs": [],
    "source": [
     "#|export\n",
-    "def urljson(url, data=None, timeout=None):\n",
+    "def urljson(url, data=None, timeout=None, headers=None):\n",
     "    \"Retrieve `url` and decode json\"\n",
-    "    res = urlread(url, data=data, timeout=timeout)\n",
+    "    res = urlread(url, data=data, timeout=timeout, headers=headers)\n",
     "    return json.loads(res) if res else {}"
    ]
   },
@@ -794,13 +795,6 @@
     "from nbdev import nbdev_export\n",
     "nbdev_export()"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {

From cd9109e0d0770032693f4f29e60922620d337b79 Mon Sep 17 00:00:00 2001
From: Carlo Lepelaars 
Date: Sat, 29 Mar 2025 13:05:29 +0100
Subject: [PATCH 069/182] Change arg order urljson for consistency

---
 fastcore/net.py   | 4 ++--
 nbs/03b_net.ipynb | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/fastcore/net.py b/fastcore/net.py
index 4effd30a..47dfebe7 100644
--- a/fastcore/net.py
+++ b/fastcore/net.py
@@ -127,9 +127,9 @@ def urlread(url, data=None, headers=None, decode=True, return_json=False, return
     return (res,dict(hdrs)) if return_headers else res
 
 # %% ../nbs/03b_net.ipynb
-def urljson(url, data=None, timeout=None, headers=None):
+def urljson(url, data=None, headers=None, timeout=None):
     "Retrieve `url` and decode json"
-    res = urlread(url, data=data, timeout=timeout, headers=headers)
+    res = urlread(url, data=data, headers=headers, timeout=timeout)
     return json.loads(res) if res else {}
 
 # %% ../nbs/03b_net.ipynb
diff --git a/nbs/03b_net.ipynb b/nbs/03b_net.ipynb
index c4cfb3fd..04bf3492 100644
--- a/nbs/03b_net.ipynb
+++ b/nbs/03b_net.ipynb
@@ -388,9 +388,9 @@
    "outputs": [],
    "source": [
     "#|export\n",
-    "def urljson(url, data=None, timeout=None, headers=None):\n",
+    "def urljson(url, data=None, headers=None, timeout=None):\n",
     "    \"Retrieve `url` and decode json\"\n",
-    "    res = urlread(url, data=data, timeout=timeout, headers=headers)\n",
+    "    res = urlread(url, data=data, headers=headers, timeout=timeout)\n",
     "    return json.loads(res) if res else {}"
    ]
   },

From 4d7c680c4ca5dd81812ffcdbf5f320ee86237d99 Mon Sep 17 00:00:00 2001
From: Carlo Lepelaars 
Date: Sat, 29 Mar 2025 13:20:25 +0100
Subject: [PATCH 070/182] consistent vertical whitespace

---
 nbs/03b_net.ipynb | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/nbs/03b_net.ipynb b/nbs/03b_net.ipynb
index 04bf3492..3c1f4a9d 100644
--- a/nbs/03b_net.ipynb
+++ b/nbs/03b_net.ipynb
@@ -795,6 +795,13 @@
     "from nbdev import nbdev_export\n",
     "nbdev_export()"
    ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
   }
  ],
  "metadata": {

From e1db8d50faf8e5a26bb81ab25142d7c9c8d3a1c4 Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Sun, 30 Mar 2025 05:34:56 +1000
Subject: [PATCH 071/182] main

---
 fastcore/_modidx.py | 2 +-
 settings.ini        | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py
index 946c7380..53a6c1f4 100644
--- a/fastcore/_modidx.py
+++ b/fastcore/_modidx.py
@@ -1,6 +1,6 @@
 # Autogenerated by nbdev
 
-d = { 'settings': { 'branch': 'master',
+d = { 'settings': { 'branch': 'main',
                 'doc_baseurl': '/',
                 'doc_host': 'https://fastcore.fast.ai',
                 'git_url': 'https://github.com/AnswerDotAI/fastcore/',
diff --git a/settings.ini b/settings.ini
index 78a34db7..a335bfd7 100644
--- a/settings.ini
+++ b/settings.ini
@@ -7,7 +7,7 @@ keywords = python
 author = Jeremy Howard and Sylvain Gugger
 author_email = infos@fast.ai
 copyright = fast.ai
-branch = master
+branch = main
 version = 1.8.1
 min_python = 3.9
 audience = Developers

From 8c83ee040947cddf74e7698b6f1f193e59d2ea15 Mon Sep 17 00:00:00 2001
From: Jonathan Whitaker 
Date: Mon, 31 Mar 2025 09:27:30 -0700
Subject: [PATCH 072/182] Add warning callout re: bool and store_true defaults

---
 nbs/06_script.ipynb | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/nbs/06_script.ipynb b/nbs/06_script.ipynb
index 905d3263..a97907c6 100644
--- a/nbs/06_script.ipynb
+++ b/nbs/06_script.ipynb
@@ -92,6 +92,21 @@
     "You should provide a default (after the `=`) for any *optional* parameters. If you don't provide a default for a parameter, then it will be a *positional* parameter."
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "::: {.callout-warning}\n",
+    "## Boolean Arguments Default to False\n",
+    "Arguments of type `bool` or `store_true` default to `False` regardless of whether you provide a default or not. Use `bool_arg` as the type instead of `bool` if you want to set a default value of True. For example:\n",
+    "\n",
+    "```python\n",
+    "@call_parse\n",
+    "def main(msg:str=\"Hi\",     # The message\n",
+    "         upper:bool_arg=True): # Convert to uppercase?\n",
+    "```"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},

From f62832e1ce367f0e7c8416a93cd0bdc9725c2004 Mon Sep 17 00:00:00 2001
From: Jonathan Whitaker 
Date: Mon, 31 Mar 2025 09:37:57 -0700
Subject: [PATCH 073/182] I should close my callout block I suppose...

---
 nbs/06_script.ipynb | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/nbs/06_script.ipynb b/nbs/06_script.ipynb
index a97907c6..54737b1b 100644
--- a/nbs/06_script.ipynb
+++ b/nbs/06_script.ipynb
@@ -104,7 +104,8 @@
     "@call_parse\n",
     "def main(msg:str=\"Hi\",     # The message\n",
     "         upper:bool_arg=True): # Convert to uppercase?\n",
-    "```"
+    "```\n",
+    ":::"
    ]
   },
   {

From 1a4430e85d1f95378fea08e210011678d68f89e3 Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Thu, 3 Apr 2025 09:06:21 +1000
Subject: [PATCH 074/182] pyproject

---
 pyproject.toml | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/pyproject.toml b/pyproject.toml
index f2c07bfb..ef86dd25 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,11 @@
 [build-system]
 requires = ["setuptools>=64.0"]
 build-backend = "setuptools.build_meta"
+
+[project]
+name="fastcore"
+requires-python=">=3.9"
+dynamic = [ "keywords", "description", "version", "dependencies", "optional-dependencies", "readme", "license", "authors", "classifiers", "entry-points", "scripts", "urls"]
+
+[tool.uv]
+cache-keys = [{ file = "pyproject.toml" }, { file = "settings.ini" }, { file = "setup.py" }]

From 13e2dea7fdf18897a4f95947aad8655593749b82 Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Sun, 6 Apr 2025 02:06:20 +1000
Subject: [PATCH 075/182] fixes #677

---
 fastcore/xml.py    |  9 +++++----
 nbs/03_xtras.ipynb | 11 +++++++++--
 nbs/09_xml.ipynb   |  9 +++++----
 nbs/nbdev.yml      |  2 +-
 4 files changed, 20 insertions(+), 11 deletions(-)

diff --git a/fastcore/xml.py b/fastcore/xml.py
index 8d19e639..35f184c6 100644
--- a/fastcore/xml.py
+++ b/fastcore/xml.py
@@ -189,15 +189,16 @@ def _to_xml(elm, lvl=0, indent=True, do_escape=True):
         if sattrs: stag += f' {sattrs}'
 
     cltag = '' if is_void else f''
+    stag_ = f'<{stag}>' if stag else ''
 
     if not cs:
-        if is_void: return f'{sp}<{stag}>{nl_end}'
-        else: return f'{sp}<{stag}>{cltag}{nl_end}'
+        if is_void: return f'{sp}{stag_}{nl_end}'
+        else: return f'{sp}{stag_}{cltag}{nl_end}'
     if len(cs) == 1 and not isinstance(cs[0], (list, tuple, FT)) and not hasattr(cs[0], '__ft__'):
         content = esc_fn(cs[0])
-        return f'{sp}<{stag}>{content}{cltag}{nl_end}'
+        return f'{sp}{stag_}{content}{cltag}{nl_end}'
 
-    res = f'{sp}<{stag}>{nl}'
+    res = f'{sp}{stag_}{nl}'
     for c in cs:
         res += _to_xml(c, lvl=lvl+2 if indent else 0, indent=indent, do_escape=do_escape)
     if not is_void: res += f'{sp}{cltag}{nl_end}'
diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb
index 73bdef45..c422e1e2 100644
--- a/nbs/03_xtras.ipynb
+++ b/nbs/03_xtras.ipynb
@@ -1914,8 +1914,15 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "from plum import Function\n",
-    "\n",
+    "from plum import Function"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
     "def f1(x): return \"Any\"\n",
     "def f2(x:int): return \"Int\"\n",
     "\n",
diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb
index ebd153c5..d3f7a158 100644
--- a/nbs/09_xml.ipynb
+++ b/nbs/09_xml.ipynb
@@ -441,15 +441,16 @@
     "        if sattrs: stag += f' {sattrs}'\n",
     "\n",
     "    cltag = '' if is_void else f''\n",
+    "    stag_ = f'<{stag}>' if stag else ''\n",
     "\n",
     "    if not cs:\n",
-    "        if is_void: return f'{sp}<{stag}>{nl_end}'\n",
-    "        else: return f'{sp}<{stag}>{cltag}{nl_end}'\n",
+    "        if is_void: return f'{sp}{stag_}{nl_end}'\n",
+    "        else: return f'{sp}{stag_}{cltag}{nl_end}'\n",
     "    if len(cs) == 1 and not isinstance(cs[0], (list, tuple, FT)) and not hasattr(cs[0], '__ft__'):\n",
     "        content = esc_fn(cs[0])\n",
-    "        return f'{sp}<{stag}>{content}{cltag}{nl_end}'\n",
+    "        return f'{sp}{stag_}{content}{cltag}{nl_end}'\n",
     "\n",
-    "    res = f'{sp}<{stag}>{nl}'\n",
+    "    res = f'{sp}{stag_}{nl}'\n",
     "    for c in cs:\n",
     "        res += _to_xml(c, lvl=lvl+2 if indent else 0, indent=indent, do_escape=do_escape)\n",
     "    if not is_void: res += f'{sp}{cltag}{nl_end}'\n",
diff --git a/nbs/nbdev.yml b/nbs/nbdev.yml
index 897ccb15..22109f4f 100644
--- a/nbs/nbdev.yml
+++ b/nbs/nbdev.yml
@@ -5,5 +5,5 @@ website:
   title: "fastcore"
   site-url: "https://fastcore.fast.ai/"
   description: "Python supercharged for fastai development"
-  repo-branch: master
+  repo-branch: main
   repo-url: "https://github.com/AnswerDotAI/fastcore/"

From 2997ce29a75228fdf4a11c0fc30913a450e99982 Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Sun, 6 Apr 2025 02:07:11 +1000
Subject: [PATCH 076/182] release

---
 CHANGELOG.md | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7687e80d..ce2dfb45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
 
 
 
+## 1.8.1
+
+### New Features
+
+- Empty tag handling in fastcore.xml to support Fragment ([#677](https://github.com/AnswerDotAI/fastcore/issues/677))
+- Add `headers` arg to `urljson` ([#675](https://github.com/AnswerDotAI/fastcore/pull/675)), thanks to [@CarloLepelaars](https://github.com/CarloLepelaars)
+- Add a setter to patch ([#674](https://github.com/AnswerDotAI/fastcore/pull/674)), thanks to [@galopyz](https://github.com/galopyz)
+
+
 ## 1.8.0
 
 ### Breaking Changes

From ca1b05e829642f01af820d4251f6ae82c331f6cd Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Sun, 6 Apr 2025 02:07:35 +1000
Subject: [PATCH 077/182] bump

---
 fastcore/__init__.py | 2 +-
 settings.ini         | 3 ++-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/fastcore/__init__.py b/fastcore/__init__.py
index 2d986fc5..320141d8 100644
--- a/fastcore/__init__.py
+++ b/fastcore/__init__.py
@@ -1 +1 @@
-__version__ = "1.8.1"
+__version__ = "1.8.2"
diff --git a/settings.ini b/settings.ini
index a335bfd7..03524974 100644
--- a/settings.ini
+++ b/settings.ini
@@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger
 author_email = infos@fast.ai
 copyright = fast.ai
 branch = main
-version = 1.8.1
+version = 1.8.2
 min_python = 3.9
 audience = Developers
 language = English
@@ -39,4 +39,5 @@ put_version_in_init = True
 cell_number = False
 skip_procs = 
 jupyter_hooks = False
+update_pyproject = True
 

From 9eb2facbd513fc25fc18f33571a8f609a9ff577c Mon Sep 17 00:00:00 2001
From: Daniel Roy Greenfeld 
Date: Fri, 2 May 2025 16:33:15 +0800
Subject: [PATCH 078/182] Make flexicache LRU instead of FIFO

---
 fastcore/xtras.py  |   3 +-
 nbs/03_xtras.ipynb | 141 +++++++++++++++++++++++++++------------------
 2 files changed, 86 insertions(+), 58 deletions(-)

diff --git a/fastcore/xtras.py b/fastcore/xtras.py
index 5d76c0c7..512cbf97 100644
--- a/fastcore/xtras.py
+++ b/fastcore/xtras.py
@@ -763,7 +763,8 @@ def _cache_logic(key, execute_func):
                 cache[key] = cache.pop(key)
                 return result
             cache[key] = (newres, [f(None) for f in funcs])
-            if len(cache) > maxsize: cache.popitem()
+            # remove the oldest item when cache overflows
+            if len(cache) > maxsize: del cache[next(iter(cache))]
             return newres
 
         @wraps(func)
diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb
index c422e1e2..f90ea594 100644
--- a/nbs/03_xtras.ipynb
+++ b/nbs/03_xtras.ipynb
@@ -154,7 +154,7 @@
        "(#5) ['./fastcore/docments.py','./fastcore/dispatch.py','./fastcore/basics.py','./fastcore/docscrape.py','./fastcore/script.py']"
       ]
      },
-     "execution_count": null,
+     "execution_count": 7,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -259,7 +259,7 @@
        "'jpeg'"
       ]
      },
-     "execution_count": null,
+     "execution_count": 12,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -688,10 +688,10 @@
     {
      "data": {
       "text/plain": [
-       "'pip 23.3.1 from /Users/jhoward/miniconda3/lib/python3.11/site-packages/pip (python 3.11)'"
+       "'pip 25.0.1 from /Users/drg/.venv/lib/python3.12/site-packages/pip (python 3.12)'"
       ]
      },
-     "execution_count": null,
+     "execution_count": 36,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1194,10 +1194,10 @@
     {
      "data": {
       "text/plain": [
-       "Path('/Users/daniel.roy.greenfeld/fh/fastcore/fastcore')"
+       "Path('/Users/drg/git/fastcore/fastcore')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 64,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1218,7 +1218,7 @@
        "Path('../fastcore')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 65,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1261,10 +1261,10 @@
     {
      "data": {
       "text/plain": [
-       "Path('000_tour.ipynb')"
+       "Path('llms.txt')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 67,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1298,7 +1298,7 @@
        "(Path('../fastcore/shutil.py'), Path('000_tour.ipynb'))"
       ]
      },
-     "execution_count": null,
+     "execution_count": 68,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1440,7 +1440,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L389){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L390){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ReindexCollection\n",
        "\n",
@@ -1451,7 +1451,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L389){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L390){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ReindexCollection\n",
        "\n",
@@ -1460,7 +1460,7 @@
        "*Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 75,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1496,7 +1496,7 @@
        "['e', 'd', 'c', 'b', 'a']"
       ]
      },
-     "execution_count": null,
+     "execution_count": 76,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1523,7 +1523,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "###### ReindexCollection.reindex\n",
        "\n",
@@ -1534,7 +1534,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "###### ReindexCollection.reindex\n",
        "\n",
@@ -1543,7 +1543,7 @@
        "*Replace `self.idxs` with idxs*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 77,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1563,7 +1563,7 @@
        "['e', 'd', 'c', 'b', 'a']"
       ]
      },
-     "execution_count": null,
+     "execution_count": 78,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1592,7 +1592,7 @@
        "CacheInfo(hits=1, misses=1, maxsize=2, currsize=1)"
       ]
      },
-     "execution_count": null,
+     "execution_count": 79,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1623,7 +1623,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L404){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L405){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.cache_clear\n",
        "\n",
@@ -1634,7 +1634,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L404){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L405){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.cache_clear\n",
        "\n",
@@ -1643,7 +1643,7 @@
        "*Clear LRU cache*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 80,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1663,7 +1663,7 @@
        "CacheInfo(hits=0, misses=0, maxsize=2, currsize=0)"
       ]
      },
-     "execution_count": null,
+     "execution_count": 81,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1688,7 +1688,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.shuffle\n",
        "\n",
@@ -1699,7 +1699,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.shuffle\n",
        "\n",
@@ -1708,7 +1708,7 @@
        "*Randomly shuffle indices*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 82,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1732,10 +1732,10 @@
     {
      "data": {
       "text/plain": [
-       "['a', 'd', 'h', 'c', 'e', 'b', 'f', 'g']"
+       "['e', 'd', 'b', 'h', 'f', 'c', 'g', 'a']"
       ]
      },
-     "execution_count": null,
+     "execution_count": 83,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1829,7 +1829,7 @@
        "2"
       ]
      },
-     "execution_count": null,
+     "execution_count": 87,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1988,10 +1988,10 @@
     {
      "data": {
       "text/plain": [
-       "'https://github.com/fastai/fastcore/tree/master/fastcore/test.py#L35'"
+       "'https://github.com/fastai/fastcore/tree/master/fastcore/test.py#L37'"
       ]
      },
-     "execution_count": null,
+     "execution_count": 95,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2109,7 +2109,7 @@
        "'▂▅▇▇'"
       ]
      },
-     "execution_count": null,
+     "execution_count": 102,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2291,7 +2291,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L530){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L533){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### EventTimer\n",
        "\n",
@@ -2302,7 +2302,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L530){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L533){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### EventTimer\n",
        "\n",
@@ -2311,7 +2311,7 @@
        "*An event timer with history of `store` items of time `span`*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 112,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2336,8 +2336,8 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Num Events: 3, Freq/sec: 205.6\n",
-      "Most recent:  ▁▁▃▁▇ 254.1 263.2 284.5 259.9 315.7\n"
+      "Num Events: 8, Freq/sec: 361.3\n",
+      "Most recent:  ▃▇▁▁▃ 267.6 322.3 233.1 227.7 269.6\n"
      ]
     }
    ],
@@ -2416,7 +2416,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L562){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L565){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PartialFormatter\n",
        "\n",
@@ -2427,7 +2427,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L562){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L565){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PartialFormatter\n",
        "\n",
@@ -2436,7 +2436,7 @@
        "*A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 118,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2499,7 +2499,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "2000-01-01 12:00:00 UTC is 2000-01-01 22:00:00+10:00 local time\n"
+      "2000-01-01 12:00:00 UTC is 2000-01-01 20:00:00+08:00 local time\n"
      ]
     }
    ],
@@ -2529,7 +2529,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "2000-01-01 12:00:00 local is 2000-01-01 02:00:00+00:00 UTC time\n"
+      "2000-01-01 12:00:00 local is 2000-01-01 04:00:00+00:00 UTC time\n"
      ]
     }
    ],
@@ -2632,7 +2632,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L619){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L622){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ContextManagers\n",
        "\n",
@@ -2643,7 +2643,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/xtras.py#L619){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L622){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ContextManagers\n",
        "\n",
@@ -2652,7 +2652,7 @@
        "*Wrapper for `contextlib.ExitStack` which enters a collection of context managers*"
       ]
      },
-     "execution_count": null,
+     "execution_count": 129,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2733,7 +2733,7 @@
        ""
       ]
      },
-     "execution_count": null,
+     "execution_count": 133,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2847,7 +2847,7 @@
        "Person(name='Bob', age=UNSET, city='Unknown')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 140,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2898,7 +2898,7 @@
        "Person(name='Bob', age=UNSET, city='NY')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 142,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2922,7 +2922,7 @@
        "Person(name='Bob', age=UNSET, city='Unknown')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 143,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2942,7 +2942,7 @@
        "Person(name='Bob', age=34, city='Unknown')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 144,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2987,7 +2987,7 @@
        "Person(name='Bob', age=UNSET, city='Unknown')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 146,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3018,7 +3018,7 @@
        "Person(name='Bob', age=UNSET, city='Unknown')"
       ]
      },
-     "execution_count": null,
+     "execution_count": 147,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3049,7 +3049,7 @@
        "True"
       ]
      },
-     "execution_count": null,
+     "execution_count": 148,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3099,7 +3099,7 @@
        "{'name': 'Bob', 'city': 'Unknown'}"
       ]
      },
-     "execution_count": null,
+     "execution_count": 150,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3187,7 +3187,8 @@
     "                cache[key] = cache.pop(key)\n",
     "                return result\n",
     "            cache[key] = (newres, [f(None) for f in funcs])\n",
-    "            if len(cache) > maxsize: cache.popitem()\n",
+    "            # remove the oldest item when cache overflows\n",
+    "            if len(cache) > maxsize: del cache[next(iter(cache))]\n",
     "            return newres\n",
     "\n",
     "        @wraps(func)\n",
@@ -3250,7 +3251,7 @@
        "3"
       ]
      },
-     "execution_count": null,
+     "execution_count": 158,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3273,7 +3274,7 @@
        "3"
       ]
      },
-     "execution_count": null,
+     "execution_count": 159,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3298,6 +3299,32 @@
     "    return flexicache(time_policy(seconds), maxsize=maxsize)"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# demonstrate that flexicache is LRU\n",
+    "@flexicache(maxsize=2)\n",
+    "def cached_func(x): return time()\n",
+    "\n",
+    "time_1 = cached_func(1)\n",
+    "test_eq(time_1, cached_func(1))\n",
+    "\n",
+    "time_2 = cached_func(2)\n",
+    "test_eq(time_1, cached_func(1))\n",
+    "test_eq(time_2, cached_func(2))\n",
+    "\n",
+    "time_3 = cached_func(3) # Removes 1\n",
+    "\n",
+    "test_eq(time_2, cached_func(2)) # cache remains\n",
+    "test_eq(time_3, cached_func(3)) # cache remains\n",
+    "test_ne(time_1, cached_func(1)) # NEQ, removes 2\n",
+    "test_ne(time_2, cached_func(2))  # NEQ, removes 3\n",
+    "test_eq(cached_func(1), cached_func(1))"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},

From 3b137ba73f7c64ee7c8ec3c90ec9bb9e1417d7e2 Mon Sep 17 00:00:00 2001
From: Daniel Roy Greenfeld 
Date: Sun, 4 May 2025 18:40:33 +0800
Subject: [PATCH 079/182] Use OrderedDict for FIFO for LRU

---
 fastcore/xtras.py  |   6 +--
 nbs/03_xtras.ipynb | 113 +++++++++++++++++++++------------------------
 2 files changed, 56 insertions(+), 63 deletions(-)

diff --git a/fastcore/xtras.py b/fastcore/xtras.py
index 512cbf97..28632d18 100644
--- a/fastcore/xtras.py
+++ b/fastcore/xtras.py
@@ -748,8 +748,9 @@ def is_namedtuple(cls):
 def flexicache(*funcs, maxsize=128):
     "Like `lru_cache`, but customisable with policy `funcs`"
     import asyncio
+    from collections import OrderedDict
     def _f(func):
-        cache,states = {}, [None]*len(funcs)
+        cache,states = OrderedDict(), [None]*len(funcs)
         def _cache_logic(key, execute_func):
             if key in cache:
                 result,states = cache[key]
@@ -763,8 +764,7 @@ def _cache_logic(key, execute_func):
                 cache[key] = cache.pop(key)
                 return result
             cache[key] = (newres, [f(None) for f in funcs])
-            # remove the oldest item when cache overflows
-            if len(cache) > maxsize: del cache[next(iter(cache))]
+            if len(cache) > maxsize: cache.popitem(last=False)
             return newres
 
         @wraps(func)
diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb
index f90ea594..e1ce4502 100644
--- a/nbs/03_xtras.ipynb
+++ b/nbs/03_xtras.ipynb
@@ -154,7 +154,7 @@
        "(#5) ['./fastcore/docments.py','./fastcore/dispatch.py','./fastcore/basics.py','./fastcore/docscrape.py','./fastcore/script.py']"
       ]
      },
-     "execution_count": 7,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -259,7 +259,7 @@
        "'jpeg'"
       ]
      },
-     "execution_count": 12,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -691,7 +691,7 @@
        "'pip 25.0.1 from /Users/drg/.venv/lib/python3.12/site-packages/pip (python 3.12)'"
       ]
      },
-     "execution_count": 36,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1197,7 +1197,7 @@
        "Path('/Users/drg/git/fastcore/fastcore')"
       ]
      },
-     "execution_count": 64,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1218,7 +1218,7 @@
        "Path('../fastcore')"
       ]
      },
-     "execution_count": 65,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1264,7 +1264,7 @@
        "Path('llms.txt')"
       ]
      },
-     "execution_count": 67,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1298,7 +1298,7 @@
        "(Path('../fastcore/shutil.py'), Path('000_tour.ipynb'))"
       ]
      },
-     "execution_count": 68,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1440,7 +1440,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L390){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L391){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ReindexCollection\n",
        "\n",
@@ -1451,7 +1451,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L390){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L391){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ReindexCollection\n",
        "\n",
@@ -1460,7 +1460,7 @@
        "*Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`*"
       ]
      },
-     "execution_count": 75,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1496,7 +1496,7 @@
        "['e', 'd', 'c', 'b', 'a']"
       ]
      },
-     "execution_count": 76,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1523,7 +1523,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "###### ReindexCollection.reindex\n",
        "\n",
@@ -1534,7 +1534,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "###### ReindexCollection.reindex\n",
        "\n",
@@ -1543,7 +1543,7 @@
        "*Replace `self.idxs` with idxs*"
       ]
      },
-     "execution_count": 77,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1563,7 +1563,7 @@
        "['e', 'd', 'c', 'b', 'a']"
       ]
      },
-     "execution_count": 78,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1592,7 +1592,7 @@
        "CacheInfo(hits=1, misses=1, maxsize=2, currsize=1)"
       ]
      },
-     "execution_count": 79,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1623,7 +1623,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L405){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.cache_clear\n",
        "\n",
@@ -1634,7 +1634,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L405){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.cache_clear\n",
        "\n",
@@ -1643,7 +1643,7 @@
        "*Clear LRU cache*"
       ]
      },
-     "execution_count": 80,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1663,7 +1663,7 @@
        "CacheInfo(hits=0, misses=0, maxsize=2, currsize=0)"
       ]
      },
-     "execution_count": 81,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1688,7 +1688,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L403){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.shuffle\n",
        "\n",
@@ -1699,7 +1699,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L403){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "##### ReindexCollection.shuffle\n",
        "\n",
@@ -1708,7 +1708,7 @@
        "*Randomly shuffle indices*"
       ]
      },
-     "execution_count": 82,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1732,10 +1732,10 @@
     {
      "data": {
       "text/plain": [
-       "['e', 'd', 'b', 'h', 'f', 'c', 'g', 'a']"
+       "['a', 'd', 'g', 'f', 'h', 'e', 'b', 'c']"
       ]
      },
-     "execution_count": 83,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1829,7 +1829,7 @@
        "2"
       ]
      },
-     "execution_count": 87,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1991,7 +1991,7 @@
        "'https://github.com/fastai/fastcore/tree/master/fastcore/test.py#L37'"
       ]
      },
-     "execution_count": 95,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2109,7 +2109,7 @@
        "'▂▅▇▇'"
       ]
      },
-     "execution_count": 102,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2291,7 +2291,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L533){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L534){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### EventTimer\n",
        "\n",
@@ -2302,7 +2302,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L533){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L534){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### EventTimer\n",
        "\n",
@@ -2311,7 +2311,7 @@
        "*An event timer with history of `store` items of time `span`*"
       ]
      },
-     "execution_count": 112,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2336,8 +2336,8 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "Num Events: 8, Freq/sec: 361.3\n",
-      "Most recent:  ▃▇▁▁▃ 267.6 322.3 233.1 227.7 269.6\n"
+      "Num Events: 1, Freq/sec: 155.1\n",
+      "Most recent:  ▇▂▂▁▁ 386.7 269.9 285.5 249.6 220.0\n"
      ]
     }
    ],
@@ -2416,7 +2416,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L565){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L566){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PartialFormatter\n",
        "\n",
@@ -2427,7 +2427,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L565){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L566){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### PartialFormatter\n",
        "\n",
@@ -2436,7 +2436,7 @@
        "*A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args*"
       ]
      },
-     "execution_count": 118,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2632,7 +2632,7 @@
       "text/markdown": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L622){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L623){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ContextManagers\n",
        "\n",
@@ -2643,7 +2643,7 @@
       "text/plain": [
        "---\n",
        "\n",
-       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L622){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
+       "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L623){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
        "\n",
        "#### ContextManagers\n",
        "\n",
@@ -2652,7 +2652,7 @@
        "*Wrapper for `contextlib.ExitStack` which enters a collection of context managers*"
       ]
      },
-     "execution_count": 129,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2733,7 +2733,7 @@
        ""
       ]
      },
-     "execution_count": 133,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2847,7 +2847,7 @@
        "Person(name='Bob', age=UNSET, city='Unknown')"
       ]
      },
-     "execution_count": 140,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2898,7 +2898,7 @@
        "Person(name='Bob', age=UNSET, city='NY')"
       ]
      },
-     "execution_count": 142,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2922,7 +2922,7 @@
        "Person(name='Bob', age=UNSET, city='Unknown')"
       ]
      },
-     "execution_count": 143,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2942,7 +2942,7 @@
        "Person(name='Bob', age=34, city='Unknown')"
       ]
      },
-     "execution_count": 144,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -2987,7 +2987,7 @@
        "Person(name='Bob', age=UNSET, city='Unknown')"
       ]
      },
-     "execution_count": 146,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3018,7 +3018,7 @@
        "Person(name='Bob', age=UNSET, city='Unknown')"
       ]
      },
-     "execution_count": 147,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3049,7 +3049,7 @@
        "True"
       ]
      },
-     "execution_count": 148,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3099,7 +3099,7 @@
        "{'name': 'Bob', 'city': 'Unknown'}"
       ]
      },
-     "execution_count": 150,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3172,8 +3172,9 @@
     "def flexicache(*funcs, maxsize=128):\n",
     "    \"Like `lru_cache`, but customisable with policy `funcs`\"\n",
     "    import asyncio\n",
+    "    from collections import OrderedDict\n",
     "    def _f(func):\n",
-    "        cache,states = {}, [None]*len(funcs)\n",
+    "        cache,states = OrderedDict(), [None]*len(funcs)\n",
     "        def _cache_logic(key, execute_func):\n",
     "            if key in cache:\n",
     "                result,states = cache[key]\n",
@@ -3187,8 +3188,7 @@
     "                cache[key] = cache.pop(key)\n",
     "                return result\n",
     "            cache[key] = (newres, [f(None) for f in funcs])\n",
-    "            # remove the oldest item when cache overflows\n",
-    "            if len(cache) > maxsize: del cache[next(iter(cache))]\n",
+    "            if len(cache) > maxsize: cache.popitem(last=False)\n",
     "            return newres\n",
     "\n",
     "        @wraps(func)\n",
@@ -3251,7 +3251,7 @@
        "3"
       ]
      },
-     "execution_count": 158,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3274,7 +3274,7 @@
        "3"
       ]
      },
-     "execution_count": 159,
+     "execution_count": null,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -3382,13 +3382,6 @@
     "#|hide\n",
     "import nbdev; nbdev.nbdev_export()"
    ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
   }
  ],
  "metadata": {

From 9e63413deef7a568b797a605d65b568f15b8b0f3 Mon Sep 17 00:00:00 2001
From: Jeremy Howard 
Date: Mon, 5 May 2025 22:41:37 +1000
Subject: [PATCH 080/182] fixes #680

---
 fastcore/_modidx.py |  1 +
 fastcore/xml.py     |  5 ++++-
 nbs/09_xml.ipynb    | 51 ++++++++++++++++++++++++++++++++++++++-------
 3 files changed, 48 insertions(+), 9 deletions(-)

diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py
index 53a6c1f4..3e9ae2a4 100644
--- a/fastcore/_modidx.py
+++ b/fastcore/_modidx.py
@@ -505,6 +505,7 @@
                               'fastcore.xml.FT.__call__': ('xml.html#ft.__call__', 'fastcore/xml.py'),
                               'fastcore.xml.FT.__getattr__': ('xml.html#ft.__getattr__', 'fastcore/xml.py'),
                               'fastcore.xml.FT.__getitem__': ('xml.html#ft.__getitem__', 'fastcore/xml.py'),
+                              'fastcore.xml.FT.__html__': ('xml.html#ft.__html__', 'fastcore/xml.py'),
                               'fastcore.xml.FT.__init__': ('xml.html#ft.__init__', 'fastcore/xml.py'),
                               'fastcore.xml.FT.__iter__': ('xml.html#ft.__iter__', 'fastcore/xml.py'),
                               'fastcore.xml.FT.__repr__': ('xml.html#ft.__repr__', 'fastcore/xml.py'),
diff --git a/fastcore/xml.py b/fastcore/xml.py
index 35f184c6..4d44f3f3 100644
--- a/fastcore/xml.py
+++ b/fastcore/xml.py
@@ -146,6 +146,7 @@ def _to_attr(k,v):
         if v==False: return ''
     if isinstance(v,str): v = escape(v, quote=False)
     elif isinstance(v, Mapping): v = json.dumps(v)
+    elif hasattr(v, '__html__'): v = v.__html__()
     else: v = str(v)
     qt = '"'
     if qt in v:
@@ -209,7 +210,9 @@ def to_xml(elm, lvl=0, indent=True, do_escape=True):
     "Convert `ft` element tree into an XML string"
     return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape))
 
-FT.__html__ = to_xml
+# %% ../nbs/09_xml.ipynb
+@patch
+def __html__(self:FT): return to_xml(self, indent=False)
 
 # %% ../nbs/09_xml.ipynb
 def highlight(s, lang='html'):
diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb
index d3f7a158..e3696a6a 100644
--- a/nbs/09_xml.ipynb
+++ b/nbs/09_xml.ipynb
@@ -382,6 +382,7 @@
     "        if v==False: return ''\n",
     "    if isinstance(v,str): v = escape(v, quote=False)\n",
     "    elif isinstance(v, Mapping): v = json.dumps(v)\n",
+    "    elif hasattr(v, '__html__'): v = v.__html__()\n",
     "    else: v = str(v)\n",
     "    qt = '\"'\n",
     "    if qt in v:\n",
@@ -460,16 +461,26 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "dd054392",
+   "id": "7a3655e9",
    "metadata": {},
    "outputs": [],
    "source": [
     "#| export\n",
     "def to_xml(elm, lvl=0, indent=True, do_escape=True):\n",
     "    \"Convert `ft` element tree into an XML string\"\n",
-    "    return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape))\n",
-    "\n",
-    "FT.__html__ = to_xml"
+    "    return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "dd054392",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "#| export\n",
+    "@patch\n",
+    "def __html__(self:FT): return to_xml(self, indent=False)"
    ]
   },
   {
@@ -640,10 +651,7 @@
      "text": [
       "
Hello from Django
\n", "\n", - "
\n", - "

Hello from fastcore <3

\n", - "
\n", - "\n" + "

Hello from fastcore <3

\n" ] } ], @@ -655,6 +663,33 @@ "print(_esc(Div(P('Hello from fastcore <3'))))" ] }, + { + "cell_type": "markdown", + "id": "d7ff740e", + "metadata": {}, + "source": [ + "FT attributes are rendered with `to_xml`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "180074af", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "

ho\">hi

\n", + "\n" + ] + } + ], + "source": [ + "print(to_xml(P('hi', value=Div('ho'))))" + ] + }, { "cell_type": "markdown", "id": "5ad30d7c", From a5d6263da340295675dff6c3fc466e66229fef59 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 5 May 2025 22:46:20 +1000 Subject: [PATCH 081/182] fixes #681 --- fastcore/xml.py | 1 + nbs/09_xml.ipynb | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/fastcore/xml.py b/fastcore/xml.py index 4d44f3f3..4209425e 100644 --- a/fastcore/xml.py +++ b/fastcore/xml.py @@ -213,6 +213,7 @@ def to_xml(elm, lvl=0, indent=True, do_escape=True): # %% ../nbs/09_xml.ipynb @patch def __html__(self:FT): return to_xml(self, indent=False) +FT.__str__ = FT.__html__ # %% ../nbs/09_xml.ipynb def highlight(s, lang='html'): diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb index e3696a6a..1d16e204 100644 --- a/nbs/09_xml.ipynb +++ b/nbs/09_xml.ipynb @@ -480,7 +480,8 @@ "source": [ "#| export\n", "@patch\n", - "def __html__(self:FT): return to_xml(self, indent=False)" + "def __html__(self:FT): return to_xml(self, indent=False)\n", + "FT.__str__ = FT.__html__" ] }, { @@ -690,6 +691,32 @@ "print(to_xml(P('hi', value=Div('ho'))))" ] }, + { + "cell_type": "markdown", + "id": "e698c66a", + "metadata": {}, + "source": [ + "FT components also stringify with `to_xml`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4f4e350", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "
ho
\n" + ] + } + ], + "source": [ + "print(Div('ho'))" + ] + }, { "cell_type": "markdown", "id": "5ad30d7c", From 58b65ba44fedad4ccbd974b16976b25bbcfd6362 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 9 May 2025 12:46:10 +1000 Subject: [PATCH 082/182] release --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce2dfb45..9e828786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ +## 1.8.2 + +### New Features + +- stringify FT with `to_xml` ([#681](https://github.com/AnswerDotAI/fastcore/issues/681)) +- render FT attrs with `to_xml` ([#680](https://github.com/AnswerDotAI/fastcore/issues/680)) +- Add sort args to delegates ([#667](https://github.com/AnswerDotAI/fastcore/pull/667)), thanks to [@pydanny](https://github.com/pydanny) + +### Bugs Squashed + +- Ensure flexicache is LRU cached ([#679](https://github.com/AnswerDotAI/fastcore/pull/679)), thanks to [@pydanny](https://github.com/pydanny) + + ## 1.8.1 ### New Features From 0fad28b8a20e437c11d70aa697659fc675656864 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 9 May 2025 12:46:23 +1000 Subject: [PATCH 083/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 320141d8..a44132de 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.2" +__version__ = "1.8.3" diff --git a/settings.ini b/settings.ini index 03524974..fae88921 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.2 +version = 1.8.3 min_python = 3.9 audience = Developers language = English From 2db6ff03c079afc110c82aa98de797261fe0be48 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 16 Jun 2025 10:14:22 +1000 Subject: [PATCH 084/182] fixes #684 --- fastcore/_modidx.py | 9 ++- fastcore/xtras.py | 27 ++++++- nbs/03_xtras.ipynb | 171 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 180 insertions(+), 27 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 3e9ae2a4..cabccafd 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -534,7 +534,13 @@ 'fastcore.xml.showtags': ('xml.html#showtags', 'fastcore/xml.py'), 'fastcore.xml.to_xml': ('xml.html#to_xml', 'fastcore/xml.py'), 'fastcore.xml.valmap': ('xml.html#valmap', 'fastcore/xml.py')}, - 'fastcore.xtras': { 'fastcore.xtras.ContextManagers': ('xtras.html#contextmanagers', 'fastcore/xtras.py'), + 'fastcore.xtras': { 'fastcore.xtras.CachedAwaitable': ('xtras.html#cachedawaitable', 'fastcore/xtras.py'), + 'fastcore.xtras.CachedAwaitable.__await__': ('xtras.html#cachedawaitable.__await__', 'fastcore/xtras.py'), + 'fastcore.xtras.CachedAwaitable.__init__': ('xtras.html#cachedawaitable.__init__', 'fastcore/xtras.py'), + 'fastcore.xtras.CachedIter': ('xtras.html#cachediter', 'fastcore/xtras.py'), + 'fastcore.xtras.CachedIter.__init__': ('xtras.html#cachediter.__init__', 'fastcore/xtras.py'), + 'fastcore.xtras.CachedIter.__iter__': ('xtras.html#cachediter.__iter__', 'fastcore/xtras.py'), + 'fastcore.xtras.ContextManagers': ('xtras.html#contextmanagers', 'fastcore/xtras.py'), 'fastcore.xtras.ContextManagers.__enter__': ('xtras.html#contextmanagers.__enter__', 'fastcore/xtras.py'), 'fastcore.xtras.ContextManagers.__exit__': ('xtras.html#contextmanagers.__exit__', 'fastcore/xtras.py'), 'fastcore.xtras.ContextManagers.__init__': ('xtras.html#contextmanagers.__init__', 'fastcore/xtras.py'), @@ -621,6 +627,7 @@ 'fastcore.xtras.open_file': ('xtras.html#open_file', 'fastcore/xtras.py'), 'fastcore.xtras.parse_env': ('xtras.html#parse_env', 'fastcore/xtras.py'), 'fastcore.xtras.partial_format': ('xtras.html#partial_format', 'fastcore/xtras.py'), + 'fastcore.xtras.reawaitable': ('xtras.html#reawaitable', 'fastcore/xtras.py'), 'fastcore.xtras.repo_details': ('xtras.html#repo_details', 'fastcore/xtras.py'), 'fastcore.xtras.repr_dict': ('xtras.html#repr_dict', 'fastcore/xtras.py'), 'fastcore.xtras.round_multiple': ('xtras.html#round_multiple', 'fastcore/xtras.py'), diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 28632d18..abd5ab3f 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -13,8 +13,8 @@ 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', - 'flexiclass', 'asdict', 'is_typeddict', 'is_namedtuple', 'flexicache', 'time_policy', 'mtime_policy', - 'timed_cache'] + 'flexiclass', 'asdict', 'is_typeddict', 'is_namedtuple', 'CachedIter', 'CachedAwaitable', 'reawaitable', + 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] # %% ../nbs/03_xtras.ipynb from .imports import * @@ -744,6 +744,29 @@ def is_namedtuple(cls): "`True` if `cls` is a namedtuple type" return issubclass(cls, tuple) and hasattr(cls, '_fields') +# %% ../nbs/03_xtras.ipynb +class CachedIter: + "Cache the result returned by an iterator" + def __init__(self, o): self.o,self.value = o,UNSET + def __iter__(self): + if self.value is UNSET: self.value = yield from self.o + return self.value + +# %% ../nbs/03_xtras.ipynb +class CachedAwaitable: + "Cache the result from an awaitable" + def __init__(self, o): self.o,self.value = o,UNSET + def __await__(self): + if self.value is UNSET: self.value = yield from self.o.__await__() + return self.value + +# %% ../nbs/03_xtras.ipynb +def reawaitable(func:callable): + "Wraps the result of an asynchronous function into an object which can be awaited more than once" + @wraps(func) + def _f(*args, **kwargs): return CachedAwaitable(func(*args, **kwargs)) + return _f + # %% ../nbs/03_xtras.ipynb def flexicache(*funcs, maxsize=128): "Like `lru_cache`, but customisable with policy `funcs`" diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index e1ce4502..55a44186 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -53,7 +53,7 @@ "from typing import TypedDict\n", "from collections import namedtuple\n", "\n", - "import shutil,tempfile,pickle,random" + "import shutil,tempfile,pickle,random,asyncio" ] }, { @@ -688,7 +688,7 @@ { "data": { "text/plain": [ - "'pip 25.0.1 from /Users/drg/.venv/lib/python3.12/site-packages/pip (python 3.12)'" + "'pip 25.1.1 from /Users/jhoward/aai-ws/.venv/lib/python3.12/site-packages/pip (python 3.12)'" ] }, "execution_count": null, @@ -1194,7 +1194,7 @@ { "data": { "text/plain": [ - "Path('/Users/drg/git/fastcore/fastcore')" + "Path('/Users/jhoward/aai-ws/fastcore/fastcore')" ] }, "execution_count": null, @@ -1440,7 +1440,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L391){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L389){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1451,7 +1451,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L391){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L389){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1523,7 +1523,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1534,7 +1534,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1623,7 +1623,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L404){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1634,7 +1634,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L404){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1688,7 +1688,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L403){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -1699,7 +1699,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L403){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -1732,7 +1732,7 @@ { "data": { "text/plain": [ - "['a', 'd', 'g', 'f', 'h', 'e', 'b', 'c']" + "['e', 'h', 'g', 'c', 'b', 'f', 'd', 'a']" ] }, "execution_count": null, @@ -2291,7 +2291,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L534){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L532){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### EventTimer\n", "\n", @@ -2302,7 +2302,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L534){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L532){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### EventTimer\n", "\n", @@ -2336,8 +2336,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Num Events: 1, Freq/sec: 155.1\n", - "Most recent: ▇▂▂▁▁ 386.7 269.9 285.5 249.6 220.0\n" + "Num Events: 13, Freq/sec: 451.7\n", + "Most recent: ▁▁▁▂▇ 264.5 257.0 278.7 293.7 363.0\n" ] } ], @@ -2416,7 +2416,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L566){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L564){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### PartialFormatter\n", "\n", @@ -2427,7 +2427,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L566){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L564){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### PartialFormatter\n", "\n", @@ -2499,7 +2499,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "2000-01-01 12:00:00 UTC is 2000-01-01 20:00:00+08:00 local time\n" + "2000-01-01 12:00:00 UTC is 2000-01-01 22:00:00+10:00 local time\n" ] } ], @@ -2529,7 +2529,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "2000-01-01 12:00:00 local is 2000-01-01 04:00:00+00:00 UTC time\n" + "2000-01-01 12:00:00 local is 2000-01-01 02:00:00+00:00 UTC time\n" ] } ], @@ -2632,7 +2632,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L623){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L621){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ContextManagers\n", "\n", @@ -2643,7 +2643,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L623){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L621){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ContextManagers\n", "\n", @@ -3162,6 +3162,115 @@ "assert not is_namedtuple(tuple)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class CachedIter:\n", + " \"Cache the result returned by an iterator\"\n", + " def __init__(self, o): self.o,self.value = o,UNSET\n", + " def __iter__(self):\n", + " if self.value is UNSET: self.value = yield from self.o\n", + " return self.value" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n" + ] + }, + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def f():\n", + " yield 1\n", + " return 2\n", + "\n", + "r = CachedIter(f())\n", + "for o in r: print(o)\n", + "r.value" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class CachedAwaitable:\n", + " \"Cache the result from an awaitable\"\n", + " def __init__(self, o): self.o,self.value = o,UNSET\n", + " def __await__(self):\n", + " if self.value is UNSET: self.value = yield from self.o.__await__()\n", + " return self.value" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def reawaitable(func:callable):\n", + " \"Wraps the result of an asynchronous function into an object which can be awaited more than once\"\n", + " @wraps(func)\n", + " def _f(*args, **kwargs): return CachedAwaitable(func(*args, **kwargs))\n", + " return _f" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`CachedCoro` and `reawaitable` are partly based on [python issue tracker](https://bugs.python.org/issue46622) code from Serhiy Storchaka. They allow an awaitable to be called multiple times." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "data\n", + "data\n" + ] + } + ], + "source": [ + "@reawaitable\n", + "async def fetch_data():\n", + " await asyncio.sleep(0.1)\n", + " return \"data\"\n", + "\n", + "r = fetch_data()\n", + "print(await r) # \"data\"\n", + "print(await r) # \"data\" (no delay)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -3207,7 +3316,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This is a flexible lru cache function that you can pass a list of functions to. Those functions define the cache eviction policy. For instance, `time_policy` is provided for time-based cache eviction, and `mtime_policy` evicts based on a file's modified-time changing. The policy functions are passed the last value that function returned was (initially `None`), and return a new value to indicate the cache has expired. When the cache expires, all functions are called with `None` to force getting new values. " + "This is a flexible lru cache function that you can pass a list of functions to. Those functions define the cache eviction policy. For instance, `time_policy` is provided for time-based cache eviction, and `mtime_policy` evicts based on a file's modified-time changing. The policy functions are passed the last value that function returned was (initially `None`), and return a new value to indicate the cache has expired. When the cache expires, all functions are called with `None` to force getting new values." ] }, { @@ -3268,6 +3377,13 @@ "execution_count": null, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n" + ] + }, { "data": { "text/plain": [ @@ -3283,7 +3399,7 @@ "@flexicache(time_policy(10), mtime_policy('000_tour.ipynb'))\n", "async def cached_func(x, y): return x+y\n", "\n", - "await cached_func(1,2)\n", + "print(await cached_func(1,2))\n", "await cached_func(1,2)" ] }, @@ -3382,6 +3498,13 @@ "#|hide\n", "import nbdev; nbdev.nbdev_export()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From 1a5cefa21ec06b3f026cec9378a22bcfdd5c2e6a Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 21 Jun 2025 09:06:54 +1000 Subject: [PATCH 085/182] fixes #685 --- fastcore/_modidx.py | 5 +++ fastcore/xtras.py | 34 +++++++++++--- nbs/03_xtras.ipynb | 107 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 8 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index cabccafd..13e7ff09 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -587,6 +587,9 @@ 'fastcore.xtras._is_property': ('xtras.html#_is_property', 'fastcore/xtras.py'), 'fastcore.xtras._property_getter': ('xtras.html#_property_getter', 'fastcore/xtras.py'), 'fastcore.xtras._repr_dict': ('xtras.html#_repr_dict', 'fastcore/xtras.py'), + 'fastcore.xtras._save_iter': ('xtras.html#_save_iter', 'fastcore/xtras.py'), + 'fastcore.xtras._save_iter.__init__': ('xtras.html#_save_iter.__init__', 'fastcore/xtras.py'), + 'fastcore.xtras._save_iter.__iter__': ('xtras.html#_save_iter.__iter__', 'fastcore/xtras.py'), 'fastcore.xtras._sparkchar': ('xtras.html#_sparkchar', 'fastcore/xtras.py'), 'fastcore.xtras._unpack': ('xtras.html#_unpack', 'fastcore/xtras.py'), 'fastcore.xtras._unwrapped_func': ('xtras.html#_unwrapped_func', 'fastcore/xtras.py'), @@ -632,6 +635,7 @@ 'fastcore.xtras.repr_dict': ('xtras.html#repr_dict', 'fastcore/xtras.py'), 'fastcore.xtras.round_multiple': ('xtras.html#round_multiple', 'fastcore/xtras.py'), 'fastcore.xtras.run': ('xtras.html#run', 'fastcore/xtras.py'), + 'fastcore.xtras.save_iter': ('xtras.html#save_iter', 'fastcore/xtras.py'), 'fastcore.xtras.save_pickle': ('xtras.html#save_pickle', 'fastcore/xtras.py'), 'fastcore.xtras.set_num_threads': ('xtras.html#set_num_threads', 'fastcore/xtras.py'), 'fastcore.xtras.shufflish': ('xtras.html#shufflish', 'fastcore/xtras.py'), @@ -640,6 +644,7 @@ 'fastcore.xtras.time_policy': ('xtras.html#time_policy', 'fastcore/xtras.py'), 'fastcore.xtras.timed_cache': ('xtras.html#timed_cache', 'fastcore/xtras.py'), 'fastcore.xtras.trace': ('xtras.html#trace', 'fastcore/xtras.py'), + 'fastcore.xtras.trim_wraps': ('xtras.html#trim_wraps', 'fastcore/xtras.py'), 'fastcore.xtras.truncstr': ('xtras.html#truncstr', 'fastcore/xtras.py'), 'fastcore.xtras.type2str': ('xtras.html#type2str', 'fastcore/xtras.py'), 'fastcore.xtras.untar_dir': ('xtras.html#untar_dir', 'fastcore/xtras.py'), diff --git a/fastcore/xtras.py b/fastcore/xtras.py index abd5ab3f..b325449c 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -9,12 +9,12 @@ __all__ = ['spark_chars', 'UNSET', 'walk', 'globtastic', 'maybe_open', 'mkdir', 'image_size', 'bunzip', 'loads', 'loads_multi', 'dumps', 'untar_dir', 'repo_details', 'run', 'open_file', 'save_pickle', 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'mapped', 'IterLen', - 'ReindexCollection', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', 'modify_exception', - 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', - 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', 'ContextManagers', - 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', - 'flexiclass', 'asdict', 'is_typeddict', 'is_namedtuple', 'CachedIter', 'CachedAwaitable', 'reawaitable', - 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] + 'ReindexCollection', 'trim_wraps', 'save_iter', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', + 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', + 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', + 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', + 'make_nullable', 'flexiclass', 'asdict', 'is_typeddict', 'is_namedtuple', 'CachedIter', 'CachedAwaitable', + 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] # %% ../nbs/03_xtras.ipynb from .imports import * @@ -409,6 +409,28 @@ def __setstate__(self, s): self.coll,self.idxs,self.cache,self.tfm = s['coll'],s shuffle="Randomly shuffle indices", cache_clear="Clear LRU cache") +# %% ../nbs/03_xtras.ipynb +def trim_wraps(f, n=1): + "Like wraps, but removes the first n parameters from the signature" + import inspect + def _(g): + g = wraps(f)(g) + sig = inspect.signature(f) + params = list(sig.parameters.values())[n:] + g.__signature__ = sig.replace(parameters=params) + return g + return _ + +# %% ../nbs/03_xtras.ipynb +class _save_iter: + def __init__(self, g, *args, **kw): self.g,self.args,self.kw = g,args,kw + def __iter__(self): yield from self.g(self, *self.args, **self.kw) + +def save_iter(g): + @trim_wraps(g) + def _(*args, **kwargs): return _save_iter(g, *args, **kwargs) + return _ + # %% ../nbs/03_xtras.ipynb def exec_eval(code, # Code to exec/eval g=None, # Globals namespace dict diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 55a44186..4d7b76e1 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -53,7 +53,7 @@ "from typing import TypedDict\n", "from collections import namedtuple\n", "\n", - "import shutil,tempfile,pickle,random,asyncio" + "import shutil,tempfile,pickle,random,asyncio,inspect" ] }, { @@ -1732,7 +1732,7 @@ { "data": { "text/plain": [ - "['e', 'h', 'g', 'c', 'b', 'f', 'd', 'a']" + "['c', 'h', 'b', 'a', 'f', 'g', 'd', 'e']" ] }, "execution_count": null, @@ -1785,6 +1785,109 @@ "## Other Helpers" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| exports\n", + "def trim_wraps(f, n=1):\n", + " \"Like wraps, but removes the first n parameters from the signature\"\n", + " import inspect\n", + " def _(g):\n", + " g = wraps(f)(g)\n", + " sig = inspect.signature(f)\n", + " params = list(sig.parameters.values())[n:]\n", + " g.__signature__ = sig.replace(parameters=params)\n", + " return g\n", + " return _" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`trim_wraps` is a decorator factory that works like `functools.wraps`, but removes the first `n` parameters from the wrapped function's signature. This is useful when creating wrapper functions that consume some parameters internally and shouldn't expose them in the public API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "adder(x, y)\n" + ] + } + ], + "source": [ + "def adder(base, x, y): return base + x + y\n", + "\n", + "def make_adder(base_value):\n", + " @trim_wraps(adder)\n", + " def _(x, y): return adder(base_value, x, y)\n", + " return _\n", + "\n", + "add_10 = make_adder(10)\n", + "print(f\"{add_10.__name__}{inspect.signature(add_10)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| exports\n", + "class _save_iter:\n", + " def __init__(self, g, *args, **kw): self.g,self.args,self.kw = g,args,kw\n", + " def __iter__(self): yield from self.g(self, *self.args, **self.kw)\n", + "\n", + "def save_iter(g):\n", + " @trim_wraps(g)\n", + " def _(*args, **kwargs): return _save_iter(g, *args, **kwargs)\n", + " return _" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`save_iter` is a decorator that allows a generator function to store values in the returned iterator object. The generator receives an object as its first parameter, which it can use to store attributes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Values: [0, 1, 2, 3, 4]\n", + "Sum stored: 10\n" + ] + } + ], + "source": [ + "@save_iter\n", + "def sum_range(self, n):\n", + " total = 0\n", + " for i in range(n):\n", + " total += i\n", + " yield i\n", + " self.value = total\n", + "\n", + "sr = sum_range(5)\n", + "print(f\"Values: {list(sr)}\")\n", + "print(f\"Sum stored: {sr.value}\") # Sum stored: 10" + ] + }, { "cell_type": "code", "execution_count": null, From c54b9d8cc66009f1a5a77f65338673105458dab0 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 21 Jun 2025 09:07:38 +1000 Subject: [PATCH 086/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e828786..32c50c45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.8.3 + +### New Features + +- Add `trim_wraps` and `save_iter` ([#685](https://github.com/AnswerDotAI/fastcore/issues/685)) +- Add `CachedIter`, `CachedAwaitable`, and `reawaitable` ([#684](https://github.com/AnswerDotAI/fastcore/issues/684)) + + ## 1.8.2 ### New Features From 1720fea3a2f34883f398ca2dfd6f440ad9c70ccd Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 21 Jun 2025 09:08:00 +1000 Subject: [PATCH 087/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index a44132de..22944760 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.3" +__version__ = "1.8.4" diff --git a/settings.ini b/settings.ini index fae88921..28e08cfd 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.3 +version = 1.8.4 min_python = 3.9 audience = Developers language = English From c2cd643fef0bff68665be38090496150340ca33a Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 21 Jun 2025 10:46:08 +1000 Subject: [PATCH 088/182] export --- nbs/03_xtras.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 4d7b76e1..ba3c18da 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -1791,7 +1791,7 @@ "metadata": {}, "outputs": [], "source": [ - "#| exports\n", + "#| export\n", "def trim_wraps(f, n=1):\n", " \"Like wraps, but removes the first n parameters from the signature\"\n", " import inspect\n", @@ -1842,7 +1842,7 @@ "metadata": {}, "outputs": [], "source": [ - "#| exports\n", + "#| export\n", "class _save_iter:\n", " def __init__(self, g, *args, **kw): self.g,self.args,self.kw = g,args,kw\n", " def __iter__(self): yield from self.g(self, *self.args, **self.kw)\n", From f19267e115cca437f44f1afbd170dab9b02b3b34 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 21 Jun 2025 11:32:31 +1000 Subject: [PATCH 089/182] fixes #686 --- fastcore/_modidx.py | 5 ++ fastcore/xtras.py | 30 +++++-- nbs/03_xtras.ipynb | 198 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 219 insertions(+), 14 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 13e7ff09..dd874615 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -578,6 +578,9 @@ 'fastcore/xtras.py'), 'fastcore.xtras.ReindexCollection.reindex': ('xtras.html#reindexcollection.reindex', 'fastcore/xtras.py'), 'fastcore.xtras.ReindexCollection.shuffle': ('xtras.html#reindexcollection.shuffle', 'fastcore/xtras.py'), + 'fastcore.xtras.SaveReturn': ('xtras.html#savereturn', 'fastcore/xtras.py'), + 'fastcore.xtras.SaveReturn.__init__': ('xtras.html#savereturn.__init__', 'fastcore/xtras.py'), + 'fastcore.xtras.SaveReturn.__iter__': ('xtras.html#savereturn.__iter__', 'fastcore/xtras.py'), 'fastcore.xtras.Unset': ('xtras.html#unset', 'fastcore/xtras.py'), 'fastcore.xtras.Unset.__bool__': ('xtras.html#unset.__bool__', 'fastcore/xtras.py'), 'fastcore.xtras.Unset.__repr__': ('xtras.html#unset.__repr__', 'fastcore/xtras.py'), @@ -588,6 +591,7 @@ 'fastcore.xtras._property_getter': ('xtras.html#_property_getter', 'fastcore/xtras.py'), 'fastcore.xtras._repr_dict': ('xtras.html#_repr_dict', 'fastcore/xtras.py'), 'fastcore.xtras._save_iter': ('xtras.html#_save_iter', 'fastcore/xtras.py'), + 'fastcore.xtras._save_iter.__aiter__': ('xtras.html#_save_iter.__aiter__', 'fastcore/xtras.py'), 'fastcore.xtras._save_iter.__init__': ('xtras.html#_save_iter.__init__', 'fastcore/xtras.py'), 'fastcore.xtras._save_iter.__iter__': ('xtras.html#_save_iter.__iter__', 'fastcore/xtras.py'), 'fastcore.xtras._sparkchar': ('xtras.html#_sparkchar', 'fastcore/xtras.py'), @@ -595,6 +599,7 @@ 'fastcore.xtras._unwrapped_func': ('xtras.html#_unwrapped_func', 'fastcore/xtras.py'), 'fastcore.xtras._unwrapped_type_dispatch_func': ( 'xtras.html#_unwrapped_type_dispatch_func', 'fastcore/xtras.py'), + 'fastcore.xtras.asave_iter': ('xtras.html#asave_iter', 'fastcore/xtras.py'), 'fastcore.xtras.asdict': ('xtras.html#asdict', 'fastcore/xtras.py'), 'fastcore.xtras.autostart': ('xtras.html#autostart', 'fastcore/xtras.py'), 'fastcore.xtras.bunzip': ('xtras.html#bunzip', 'fastcore/xtras.py'), diff --git a/fastcore/xtras.py b/fastcore/xtras.py index b325449c..79371999 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -9,12 +9,13 @@ __all__ = ['spark_chars', 'UNSET', 'walk', 'globtastic', 'maybe_open', 'mkdir', 'image_size', 'bunzip', 'loads', 'loads_multi', 'dumps', 'untar_dir', 'repo_details', 'run', 'open_file', 'save_pickle', 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'mapped', 'IterLen', - 'ReindexCollection', 'trim_wraps', 'save_iter', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', - 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', - 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', - 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', - 'make_nullable', 'flexiclass', 'asdict', 'is_typeddict', 'is_namedtuple', 'CachedIter', 'CachedAwaitable', - 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] + 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', 'exec_eval', 'get_source_link', + 'truncstr', 'sparkline', 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', + 'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', + 'trace', 'modified_env', 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', + 'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', 'flexiclass', 'asdict', 'is_typeddict', + 'is_namedtuple', 'CachedIter', 'CachedAwaitable', 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', + 'timed_cache'] # %% ../nbs/03_xtras.ipynb from .imports import * @@ -409,6 +410,14 @@ def __setstate__(self, s): self.coll,self.idxs,self.cache,self.tfm = s['coll'],s shuffle="Randomly shuffle indices", cache_clear="Clear LRU cache") +# %% ../nbs/03_xtras.ipynb +class SaveReturn: + "Wrap an iterator such that the generator function's return value is stored in `.value`" + def __init__(self, its): self.its = its + def __iter__(self): + self.value = yield from self.its + return self.value + # %% ../nbs/03_xtras.ipynb def trim_wraps(f, n=1): "Like wraps, but removes the first n parameters from the signature" @@ -425,8 +434,17 @@ def _(g): class _save_iter: def __init__(self, g, *args, **kw): self.g,self.args,self.kw = g,args,kw def __iter__(self): yield from self.g(self, *self.args, **self.kw) + def __aiter__(self): return self.g(self, *self.args, **self.kw) def save_iter(g): + "Decorator that allows a generator function to store values in the returned iterator object" + @trim_wraps(g) + def _(*args, **kwargs): return _save_iter(g, *args, **kwargs) + return _ + +# %% ../nbs/03_xtras.ipynb +def asave_iter(g): + "Like `save_iter`, but for async iterators" @trim_wraps(g) def _(*args, **kwargs): return _save_iter(g, *args, **kwargs) return _ diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index ba3c18da..5c643c20 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -1732,7 +1732,7 @@ { "data": { "text/plain": [ - "['c', 'h', 'b', 'a', 'f', 'g', 'd', 'e']" + "['g', 'c', 'f', 'd', 'a', 'b', 'e', 'h']" ] }, "execution_count": null, @@ -1782,7 +1782,99 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Other Helpers" + "## `SaveReturn` and `save_iter` Variants" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "These utilities solve a common problem in Python: how to extract additional information from generator functions beyond just the yielded values.\n", + "\n", + "In Python, generator functions can `yield` values and also `return` a final value, but the return value is normally lost when you iterate over the generator:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def example_generator():\n", + " total = 0\n", + " for i in range(3):\n", + " total += i\n", + " yield i\n", + " return total # This gets lost!\n", + "\n", + "# The return value (3) is lost\n", + "values = list(example_generator()) # [0, 1, 2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| exports\n", + "class SaveReturn:\n", + " \"Wrap an iterator such that the generator function's return value is stored in `.value`\"\n", + " def __init__(self, its): self.its = its\n", + " def __iter__(self):\n", + " self.value = yield from self.its\n", + " return self.value" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`SaveReturn` is the simplest approach to solving this problem - it wraps any existing (non-async) generator and captures its return value. This works because `yield from` (used internally in `SaveReturn`) returns the value from the `return` of the generator function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Values: [0, 1, 2, 3, 4]\n" + ] + }, + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def sum_range(n):\n", + " total = 0\n", + " for i in range(n):\n", + " total += i\n", + " yield i\n", + " return total # This value is returned by yield from\n", + "\n", + "sr = SaveReturn(sum_range(5))\n", + "values = list(sr) # This will consume the generator and get the return value\n", + "print(f\"Values: {values}\")\n", + "sr.value" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to provide an accurate signature for `save_iter`, we need a version of `wraps` that removes leading parameters:" ] }, { @@ -1846,8 +1938,91 @@ "class _save_iter:\n", " def __init__(self, g, *args, **kw): self.g,self.args,self.kw = g,args,kw\n", " def __iter__(self): yield from self.g(self, *self.args, **self.kw)\n", + " def __aiter__(self): return self.g(self, *self.args, **self.kw)\n", "\n", "def save_iter(g):\n", + " \"Decorator that allows a generator function to store values in the returned iterator object\"\n", + " @trim_wraps(g)\n", + " def _(*args, **kwargs): return _save_iter(g, *args, **kwargs)\n", + " return _" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`save_iter` modifies generator functions to store state in the iterator object itself. The generator receives an object as its first parameter, which it can use to store attributes. You can store values during iteration, not just at the end,\n", + "and you can store multiple attributes if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@save_iter\n", + "def sum_range(o, n): # Note: 'o' parameter added\n", + " total = 0\n", + " for i in range(n):\n", + " total += i\n", + " yield i\n", + " o.value = total # Store directly on the iterator object" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Because iternally `save_iter` uses `trim_wraps`, the signature of `sum_range` correctly shows that you should *not* pass `o` to it; it's injected by the decorating function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(n)\n" + ] + } + ], + "source": [ + "print(sum_range.__signature__)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Values: [0, 1, 2, 3, 4]\n", + "Sum stored: 10\n" + ] + } + ], + "source": [ + "sr = sum_range(5)\n", + "print(f\"Values: {list(sr)}\")\n", + "print(f\"Sum stored: {sr.value}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def asave_iter(g):\n", + " \"Like `save_iter`, but for async iterators\"\n", " @trim_wraps(g)\n", " def _(*args, **kwargs): return _save_iter(g, *args, **kwargs)\n", " return _" @@ -1857,7 +2032,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "`save_iter` is a decorator that allows a generator function to store values in the returned iterator object. The generator receives an object as its first parameter, which it can use to store attributes." + "`asave_iter` provides the same functionality as `save_iter`, but for async generator functions. `yield from` and `return` can not be used with async generator functions, so `SaveReturn` can't be used here." ] }, { @@ -1875,17 +2050,24 @@ } ], "source": [ - "@save_iter\n", - "def sum_range(self, n):\n", + "@asave_iter\n", + "async def asum_range(self, n):\n", " total = 0\n", " for i in range(n):\n", " total += i\n", " yield i\n", " self.value = total\n", "\n", - "sr = sum_range(5)\n", - "print(f\"Values: {list(sr)}\")\n", - "print(f\"Sum stored: {sr.value}\") # Sum stored: 10" + "asr = asum_range(5)\n", + "print(f\"Values: {[o async for o in asr]}\")\n", + "print(f\"Sum stored: {asr.value}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Other Helpers" ] }, { From fc29d281e4cb6d4b6e659b1e710673e3f239567f Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 21 Jun 2025 11:32:48 +1000 Subject: [PATCH 090/182] release --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c50c45..e1e82abb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ -## 1.8.3 +## 1.8.4 ### New Features - Add `trim_wraps` and `save_iter` ([#685](https://github.com/AnswerDotAI/fastcore/issues/685)) +- Add `asave_iter` ([#686](https://github.com/AnswerDotAI/fastcore/issues/686)) - Add `CachedIter`, `CachedAwaitable`, and `reawaitable` ([#684](https://github.com/AnswerDotAI/fastcore/issues/684)) From 53c5c8b50d87119ebbb73bdf3c1969bfb7bb2ced Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 21 Jun 2025 11:33:02 +1000 Subject: [PATCH 091/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 22944760..89c6ad8e 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.4" +__version__ = "1.8.5" diff --git a/settings.ini b/settings.ini index 28e08cfd..3ff0d9a8 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.4 +version = 1.8.5 min_python = 3.9 audience = Developers language = English From 08914733cf3d409b9b416f80213e4d0d01c131ad Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 23 Jun 2025 15:17:58 +1000 Subject: [PATCH 092/182] unbump --- CHANGELOG.md | 4 ++++ fastcore/__init__.py | 2 +- settings.ini | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1e82abb..84da492f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ + + + + ## 1.8.4 ### New Features diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 89c6ad8e..22944760 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.5" +__version__ = "1.8.4" diff --git a/settings.ini b/settings.ini index 3ff0d9a8..28e08cfd 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.5 +version = 1.8.4 min_python = 3.9 audience = Developers language = English From 538c968fcf4eae2c36cce06ba695f500c3222a87 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 23 Jun 2025 15:18:12 +1000 Subject: [PATCH 093/182] release --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84da492f..00817774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,6 @@ - - - ## 1.8.4 ### New Features From e628b4b959b65b89be3466618cd4f3d04a60e6cc Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 23 Jun 2025 15:18:51 +1000 Subject: [PATCH 094/182] rebump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 22944760..89c6ad8e 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.4" +__version__ = "1.8.5" diff --git a/settings.ini b/settings.ini index 28e08cfd..3ff0d9a8 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.4 +version = 1.8.5 min_python = 3.9 audience = Developers language = English From 9937237bcd6c28c8d9da56a2f4c3e7e6d9391827 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 30 Jun 2025 10:17:00 +1000 Subject: [PATCH 095/182] fixes #687 --- fastcore/_modidx.py | 3 +- fastcore/docments.py | 28 +++++++++++++++-- nbs/04_docments.ipynb | 73 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index dd874615..85ab1938 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -253,7 +253,8 @@ 'fastcore.docments.get_source': ('docments.html#get_source', 'fastcore/docments.py'), 'fastcore.docments.isdataclass': ('docments.html#isdataclass', 'fastcore/docments.py'), 'fastcore.docments.parse_docstring': ('docments.html#parse_docstring', 'fastcore/docments.py'), - 'fastcore.docments.qual_name': ('docments.html#qual_name', 'fastcore/docments.py')}, + 'fastcore.docments.qual_name': ('docments.html#qual_name', 'fastcore/docments.py'), + 'fastcore.docments.sig2str': ('docments.html#sig2str', 'fastcore/docments.py')}, 'fastcore.docscrape': {}, 'fastcore.foundation': { 'fastcore.foundation.CollBase': ('foundation.html#collbase', 'fastcore/foundation.py'), 'fastcore.foundation.CollBase.__delitem__': ( 'foundation.html#collbase.__delitem__', diff --git a/fastcore/docments.py b/fastcore/docments.py index 2aa53db3..9b18d0ba 100644 --- a/fastcore/docments.py +++ b/fastcore/docments.py @@ -5,7 +5,7 @@ # %% ../nbs/04_docments.ipynb 2 from __future__ import annotations -import re,ast +import re,ast,inspect from tokenize import tokenize,COMMENT from ast import parse,FunctionDef,AsyncFunctionDef,AnnAssign from io import BytesIO @@ -20,7 +20,7 @@ # %% auto 0 __all__ = ['empty', 'docstring', 'parse_docstring', 'isdataclass', 'get_dataclass_source', 'get_source', 'get_name', 'qual_name', - 'docments', 'extract_docstrings'] + 'docments', 'sig2str', 'extract_docstrings'] # %% ../nbs/04_docments.ipynb def docstring(sym): @@ -180,6 +180,29 @@ def _update_docments(f, r): if not full: r = {k:v['docment'] for k,v in r.items()} return AttrDict(r) +# %% ../nbs/04_docments.ipynb +def sig2str(func): + "Generate function signature with docments as comments" + docs = docments(func, full=True) + params = [] + for k,v in docs.items(): + if k == 'return': continue + anno = getattr(v['anno'], '__name__', str(v['anno'])) if v['anno'] != inspect._empty else '' + if '|' in str(v['anno']): anno = str(v['anno']) + p = k + (f':{anno}' if anno and anno != 'inspect._empty' else '') + if v['default'] != inspect._empty: + d = getattr(v['default'], '__name__', v['default']) if hasattr(v['default'], '__name__') else v['default'] + p += f'={d}' if d is not None else '=None' + if v['docment']: p += f' # {v["docment"]}' + params.append(p) + + ret = docs.get('return', {}) + ret_str = ':' + if ret and ret.get('anno')!=inspect._empty: + ret_str = f"->{getattr(ret['anno'], '__name__', str(ret['anno']))}" + (f': # {ret["docment"]}' if ret.get('docment') else ':') + doc_str = f' "{func.__doc__}"' if func.__doc__ else '' + return f"def {func.__name__}(\n " + ",\n ".join(params) + f"\n){ret_str}\n{doc_str}" + # %% ../nbs/04_docments.ipynb def _get_params(node): params = [a.arg for a in node.args.args] @@ -187,6 +210,7 @@ def _get_params(node): if node.args.kwarg: params.append(f"**{node.args.kwarg.arg}") return ", ".join(params) +# %% ../nbs/04_docments.ipynb class _DocstringExtractor(ast.NodeVisitor): def __init__(self): self.docstrings,self.cls,self.cls_init = {},None,None diff --git a/nbs/04_docments.ipynb b/nbs/04_docments.ipynb index 21817d59..143827c2 100644 --- a/nbs/04_docments.ipynb +++ b/nbs/04_docments.ipynb @@ -27,7 +27,7 @@ "#|export\n", "from __future__ import annotations\n", "\n", - "import re,ast\n", + "import re,ast,inspect\n", "from tokenize import tokenize,COMMENT\n", "from ast import parse,FunctionDef,AsyncFunctionDef,AnnAssign\n", "from io import BytesIO\n", @@ -840,7 +840,7 @@ "output_type": "stream", "text": [ "The sum of two numbers.\n", - " \n", + "\n", " Used to demonstrate numpy-style docstrings.\n", "\n", "Parameters\n", @@ -1019,7 +1019,8 @@ "\n", "@delegates(_a)\n", "def _b(b:str, # Second\n", - " **kwargs): \n", + " **kwargs\n", + " ): # Return nothing\n", " return b, (_a(**kwargs)) \n", "\n", "docments(_b)" @@ -1073,7 +1074,7 @@ "@delegates(_c)\n", "def _d(c:int, # First\n", " b:str, **kwargs\n", - " )->int:\n", + " )->int: # Return an int\n", " return c, _c(b, **kwargs)" ] }, @@ -1090,6 +1091,58 @@ "test_eq(docments(_d, full=True).keys() & _argset, _argset) # _d has the args a,b,c and return" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def sig2str(func):\n", + " \"Generate function signature with docments as comments\"\n", + " docs = docments(func, full=True)\n", + " params = []\n", + " for k,v in docs.items():\n", + " if k == 'return': continue\n", + " anno = getattr(v['anno'], '__name__', str(v['anno'])) if v['anno'] != inspect._empty else ''\n", + " if '|' in str(v['anno']): anno = str(v['anno'])\n", + " p = k + (f':{anno}' if anno and anno != 'inspect._empty' else '')\n", + " if v['default'] != inspect._empty:\n", + " d = getattr(v['default'], '__name__', v['default']) if hasattr(v['default'], '__name__') else v['default']\n", + " p += f'={d}' if d is not None else '=None'\n", + " if v['docment']: p += f' # {v[\"docment\"]}'\n", + " params.append(p)\n", + " \n", + " ret = docs.get('return', {})\n", + " ret_str = ':'\n", + " if ret and ret.get('anno')!=inspect._empty:\n", + " ret_str = f\"->{getattr(ret['anno'], '__name__', str(ret['anno']))}\" + (f': # {ret[\"docment\"]}' if ret.get('docment') else ':')\n", + " doc_str = f' \"{func.__doc__}\"' if func.__doc__ else ''\n", + " return f\"def {func.__name__}(\\n \" + \",\\n \".join(params) + f\"\\n){ret_str}\\n{doc_str}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "def _d(\n", + " b:str # Second,\n", + " a:int=2 # Third,\n", + " c:int # First\n", + ")->int: # Return an int\n", + "\n" + ] + } + ], + "source": [ + "print(sig2str(_d))" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1108,8 +1161,16 @@ " params = [a.arg for a in node.args.args]\n", " if node.args.vararg: params.append(f\"*{node.args.vararg.arg}\")\n", " if node.args.kwarg: params.append(f\"**{node.args.kwarg.arg}\")\n", - " return \", \".join(params)\n", - "\n", + " return \", \".join(params)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", "class _DocstringExtractor(ast.NodeVisitor):\n", " def __init__(self): self.docstrings,self.cls,self.cls_init = {},None,None\n", "\n", From 90771e198c2cfdf28189d913562ab7a09902587e Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 30 Jun 2025 15:03:01 +1000 Subject: [PATCH 096/182] add docments to all --- fastcore/all.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastcore/all.py b/fastcore/all.py index 99babb3a..91a90771 100644 --- a/fastcore/all.py +++ b/fastcore/all.py @@ -7,4 +7,5 @@ from .meta import * from .imports import * from .script import * +from .docments import * from .xml import * From 9ecdcf5ec39a91707322854d0da854e8296d3183 Mon Sep 17 00:00:00 2001 From: Kerem Turgutlu Date: Mon, 30 Jun 2025 18:52:16 +0300 Subject: [PATCH 097/182] test jpeg with COM segment --- fastcore/imghdr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/imghdr.py b/fastcore/imghdr.py index 91b84923..c1a5031b 100644 --- a/fastcore/imghdr.py +++ b/fastcore/imghdr.py @@ -36,8 +36,8 @@ def what(file, h=None): tests = [] def test_jpeg(h, f): - """JPEG data with JFIF or Exif markers; and raw JPEG""" - if h[6:10] in (b'JFIF', b'Exif') or h[:4] in (b'\xff\xd8\xff\xdb',b'\xff\xd8\xff\xe2',b'\xff\xd8\xff\xe1'): + """JPEG data with JFIF or Exif markers; and raw JPEG including COM segments""" + if h[6:10] in (b'JFIF', b'Exif') or h[:4] in (b'\xff\xd8\xff\xdb',b'\xff\xd8\xff\xe2',b'\xff\xd8\xff\xe1',b'\xff\xd8\xff\xfe'): return 'jpeg' tests.append(test_jpeg) From e5a0bae2303fb98c67262c2ebc9c006141c50019 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 8 Jul 2025 16:17:08 +1000 Subject: [PATCH 098/182] fixes #689 --- fastcore/xml.py | 5 ++++- nbs/09_xml.ipynb | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/fastcore/xml.py b/fastcore/xml.py index 4209425e..7721a3d4 100644 --- a/fastcore/xml.py +++ b/fastcore/xml.py @@ -208,7 +208,10 @@ def _to_xml(elm, lvl=0, indent=True, do_escape=True): # %% ../nbs/09_xml.ipynb def to_xml(elm, lvl=0, indent=True, do_escape=True): "Convert `ft` element tree into an XML string" - return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape)) + if isinstance(elm, (list,tuple,FT)) or hasattr(elm, '__ft__'): + return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape)) + if isinstance(elm, bytes): return elm.decode('utf-8') + return elm or '' # %% ../nbs/09_xml.ipynb @patch diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb index 1d16e204..0c9811d4 100644 --- a/nbs/09_xml.ipynb +++ b/nbs/09_xml.ipynb @@ -468,7 +468,10 @@ "#| export\n", "def to_xml(elm, lvl=0, indent=True, do_escape=True):\n", " \"Convert `ft` element tree into an XML string\"\n", - " return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape))" + " if isinstance(elm, (list,tuple,FT)) or hasattr(elm, '__ft__'):\n", + " return Safe(_to_xml(elm, lvl, indent, do_escape=do_escape))\n", + " if isinstance(elm, bytes): return elm.decode('utf-8')\n", + " return elm or ''" ] }, { From edd3a939a70ddd05fc5e17247276324bb49ed552 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 8 Jul 2025 16:17:24 +1000 Subject: [PATCH 099/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00817774..f3214fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.8.5 + +### New Features + +- Auto skip non usable types in `to_xml` ([#689](https://github.com/AnswerDotAI/fastcore/issues/689)) +- Handle jpeg with COM segment ([#688](https://github.com/AnswerDotAI/fastcore/pull/688)), thanks to [@KeremTurgutlu](https://github.com/KeremTurgutlu) +- Add `sig2str` ([#687](https://github.com/AnswerDotAI/fastcore/issues/687)) + ## 1.8.4 From e194a6711c67ffea25c275275e0c2a93d9597505 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 8 Jul 2025 16:17:39 +1000 Subject: [PATCH 100/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 89c6ad8e..17ecd62a 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.5" +__version__ = "1.8.6" diff --git a/settings.ini b/settings.ini index 3ff0d9a8..c1fc8bd0 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.5 +version = 1.8.6 min_python = 3.9 audience = Developers language = English From df9f82f1ec53378317f0e20df8e0a3c597c703c1 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Thu, 24 Jul 2025 09:02:53 +1000 Subject: [PATCH 101/182] fixes #690 --- fastcore/test.py | 9 ++-- nbs/00_test.ipynb | 135 +++++++++++++++++++++++++++++++--------------- 2 files changed, 97 insertions(+), 47 deletions(-) diff --git a/fastcore/test.py b/fastcore/test.py index dfe63b04..b324cc23 100644 --- a/fastcore/test.py +++ b/fastcore/test.py @@ -13,14 +13,15 @@ from contextlib import redirect_stdout # %% ../nbs/00_test.ipynb -def test_fail(f, msg='', contains='', args=None, kwargs=None): - "Fails with `msg` unless `f()` raises an exception and (optionally) has `contains` in `e.args`" +def test_fail(f, msg='', contains='', exc=Exception, args=None, kwargs=None): + "Fails with `msg` unless `f()` raises an exception of type `exc` and (optionally) has `contains` in `e.args`" args, kwargs = args or [], kwargs or {} try: f(*args, **kwargs) - except Exception as e: + except exc as e: assert not contains or contains in str(e) return - assert False,f"Expected exception but none raised. {msg}" + except Exception as e: assert False, f"Expected {exc.__name__} but got {type(e).__name__}: {e}. {msg}" + assert False, f"Expected {exc.__name__} but none raised. {msg}" # %% ../nbs/00_test.ipynb def test(a, b, cmp, cname=None): diff --git a/nbs/00_test.ipynb b/nbs/00_test.ipynb index 0ce004f3..95960e9c 100644 --- a/nbs/00_test.ipynb +++ b/nbs/00_test.ipynb @@ -3,6 +3,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ccfdfbb9", "metadata": {}, "outputs": [], "source": [ @@ -12,10 +13,11 @@ { "cell_type": "code", "execution_count": null, + "id": "76d8df3a", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from fastcore.imports import *\n", "from collections import Counter\n", "from contextlib import redirect_stdout" @@ -24,6 +26,7 @@ { "cell_type": "code", "execution_count": null, + "id": "54c5392a", "metadata": {}, "outputs": [], "source": [ @@ -34,6 +37,7 @@ }, { "cell_type": "markdown", + "id": "b0d9a633", "metadata": {}, "source": [ "# Test\n", @@ -43,6 +47,7 @@ }, { "cell_type": "markdown", + "id": "855b08fd", "metadata": {}, "source": [ "## Simple test functions" @@ -50,6 +55,7 @@ }, { "cell_type": "markdown", + "id": "341909fc", "metadata": {}, "source": [ "We can check that code raises an exception when that's expected (`test_fail`).\n", @@ -60,23 +66,26 @@ { "cell_type": "code", "execution_count": null, + "id": "2927d8a7", "metadata": {}, "outputs": [], "source": [ - "#|export\n", - "def test_fail(f, msg='', contains='', args=None, kwargs=None):\n", - " \"Fails with `msg` unless `f()` raises an exception and (optionally) has `contains` in `e.args`\"\n", + "#| export\n", + "def test_fail(f, msg='', contains='', exc=Exception, args=None, kwargs=None):\n", + " \"Fails with `msg` unless `f()` raises an exception of type `exc` and (optionally) has `contains` in `e.args`\"\n", " args, kwargs = args or [], kwargs or {}\n", " try: f(*args, **kwargs)\n", - " except Exception as e:\n", + " except exc as e:\n", " assert not contains or contains in str(e)\n", " return\n", - " assert False,f\"Expected exception but none raised. {msg}\"" + " except Exception as e: assert False, f\"Expected {exc.__name__} but got {type(e).__name__}: {e}. {msg}\"\n", + " assert False, f\"Expected {exc.__name__} but none raised. {msg}\"" ] }, { "cell_type": "code", "execution_count": null, + "id": "af610e8b", "metadata": {}, "outputs": [], "source": [ @@ -84,11 +93,16 @@ "test_fail(_fail, contains=\"foo\")\n", "\n", "def _fail(): raise Exception()\n", - "test_fail(_fail)" + "test_fail(_fail)\n", + "\n", + "def _fail(): raise ValueError()\n", + "test_fail(_fail, exc=ValueError)\n", + "test_fail(lambda: test_fail(_fail, exc=IndexError), exc=AssertionError)\n" ] }, { "cell_type": "markdown", + "id": "2cc4d553", "metadata": {}, "source": [ "We can also pass `args` and `kwargs` to function to check if it fails with special inputs." @@ -97,6 +111,7 @@ { "cell_type": "code", "execution_count": null, + "id": "180a2711", "metadata": {}, "outputs": [], "source": [ @@ -110,10 +125,11 @@ { "cell_type": "code", "execution_count": null, + "id": "a61150ba", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test(a, b, cmp, cname=None):\n", " \"`assert` that `cmp(a,b)`; display inputs and `cname or cmp.__name__` if it fails\"\n", " if cname is None: cname=cmp.__name__\n", @@ -123,6 +139,7 @@ { "cell_type": "code", "execution_count": null, + "id": "23cae366", "metadata": {}, "outputs": [], "source": [ @@ -135,6 +152,7 @@ { "cell_type": "code", "execution_count": null, + "id": "28434d1f", "metadata": {}, "outputs": [ { @@ -170,6 +188,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7d39b942", "metadata": {}, "outputs": [], "source": [ @@ -180,6 +199,7 @@ { "cell_type": "code", "execution_count": null, + "id": "eac3775c", "metadata": {}, "outputs": [ { @@ -215,6 +235,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1c123b0d", "metadata": {}, "outputs": [], "source": [ @@ -225,10 +246,11 @@ { "cell_type": "code", "execution_count": null, + "id": "ff22e983", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def nequals(a,b):\n", " \"Compares `a` and `b` for `not equals`\"\n", " return not equals(a,b)" @@ -237,6 +259,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7f2d6103", "metadata": {}, "outputs": [], "source": [ @@ -245,6 +268,7 @@ }, { "cell_type": "markdown", + "id": "d42f4f8d", "metadata": {}, "source": [ "## test_eq test_ne, etc..." @@ -252,6 +276,7 @@ }, { "cell_type": "markdown", + "id": "7ed5aded", "metadata": {}, "source": [ "Just use `test_eq`/`test_ne` to test for `==`/`!=`. `test_eq_type` checks things are equal and of the same type. We define them using `test`:" @@ -260,10 +285,11 @@ { "cell_type": "code", "execution_count": null, + "id": "50754983", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_eq(a,b):\n", " \"`test` that `a==b`\"\n", " test(a,b,equals, cname='==')" @@ -272,9 +298,8 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "hide_input": false - }, + "id": "bb2345e6", + "metadata": {}, "outputs": [], "source": [ "test_eq([1,2],[1,2])\n", @@ -291,6 +316,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4226d9bd", "metadata": {}, "outputs": [], "source": [ @@ -302,6 +328,7 @@ { "cell_type": "code", "execution_count": null, + "id": "dcbbbf07", "metadata": {}, "outputs": [], "source": [ @@ -319,6 +346,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b87d91f1", "metadata": {}, "outputs": [], "source": [ @@ -331,10 +359,11 @@ { "cell_type": "code", "execution_count": null, + "id": "306fa8cf", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_eq_type(a,b):\n", " \"`test` that `a==b` and are same type\"\n", " test_eq(a,b)\n", @@ -345,9 +374,8 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "hide_input": false - }, + "id": "9ae1642c", + "metadata": {}, "outputs": [], "source": [ "test_eq_type(1,1)\n", @@ -360,10 +388,11 @@ { "cell_type": "code", "execution_count": null, + "id": "884adc8c", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_ne(a,b):\n", " \"`test` that `a!=b`\"\n", " test(a,b,nequals,'!=')" @@ -372,9 +401,8 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "hide_input": false - }, + "id": "263c3954", + "metadata": {}, "outputs": [], "source": [ "test_ne([1,2],[1])\n", @@ -391,10 +419,11 @@ { "cell_type": "code", "execution_count": null, + "id": "25739b8f", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def is_close(a,b,eps=1e-5):\n", " \"Is `a` within `eps` of `b`\"\n", " if hasattr(a, '__array__') or hasattr(b,'__array__'):\n", @@ -407,10 +436,11 @@ { "cell_type": "code", "execution_count": null, + "id": "e3662776", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_close(a,b,eps=1e-5):\n", " \"`test` that `a` is within `eps` of `b`\"\n", " test(a,b,partial(is_close,eps=eps),'close')" @@ -419,6 +449,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e17167a4", "metadata": {}, "outputs": [], "source": [ @@ -432,10 +463,11 @@ { "cell_type": "code", "execution_count": null, + "id": "eb62013c", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_is(a,b):\n", " \"`test` that `a is b`\"\n", " test(a,b,operator.is_, 'is')" @@ -444,6 +476,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0dd1537f", "metadata": {}, "outputs": [], "source": [ @@ -456,10 +489,11 @@ { "cell_type": "code", "execution_count": null, + "id": "cde9c1f2", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_shuffled(a,b):\n", " \"`test` that `a` and `b` are shuffled versions of the same sequence of items\"\n", " test_ne(a, b)\n", @@ -469,6 +503,7 @@ { "cell_type": "code", "execution_count": null, + "id": "289a527b", "metadata": {}, "outputs": [], "source": [ @@ -482,6 +517,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4f319876", "metadata": {}, "outputs": [], "source": [ @@ -493,6 +529,7 @@ { "cell_type": "code", "execution_count": null, + "id": "992349d1", "metadata": {}, "outputs": [], "source": [ @@ -504,10 +541,11 @@ { "cell_type": "code", "execution_count": null, + "id": "771cf3e9", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_stdout(f, exp, regex=False):\n", " \"Test that `f` prints `exp` to stdout, optionally checking as `regex`\"\n", " s = io.StringIO()\n", @@ -519,6 +557,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7214de9b", "metadata": {}, "outputs": [], "source": [ @@ -531,10 +570,11 @@ { "cell_type": "code", "execution_count": null, + "id": "41b5f286", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_warns(f, show=False):\n", " with warnings.catch_warnings(record=True) as w:\n", " f()\n", @@ -546,6 +586,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1af9bfb5", "metadata": {}, "outputs": [], "source": [ @@ -556,6 +597,7 @@ { "cell_type": "code", "execution_count": null, + "id": "059c9439", "metadata": {}, "outputs": [ { @@ -573,16 +615,18 @@ { "cell_type": "code", "execution_count": null, + "id": "6283b7f9", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "TEST_IMAGE = 'images/puppy.jpg'" ] }, { "cell_type": "code", "execution_count": null, + "id": "863d38b5", "metadata": {}, "outputs": [ { @@ -605,16 +649,18 @@ { "cell_type": "code", "execution_count": null, + "id": "1c01ca5d", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "TEST_IMAGE_BW = 'images/mnist3.png'" ] }, { "cell_type": "code", "execution_count": null, + "id": "6501930e", "metadata": {}, "outputs": [ { @@ -637,10 +683,11 @@ { "cell_type": "code", "execution_count": null, + "id": "0de4e727", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_fig_exists(ax):\n", " \"Test there is a figure displayed in `ax`\"\n", " if not hasattr(ax.figure.canvas, 'renderer'): ax.figure.canvas.draw()\n", @@ -650,6 +697,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d814ce13", "metadata": {}, "outputs": [ { @@ -671,6 +719,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d8a43270", "metadata": {}, "outputs": [], "source": [ @@ -680,10 +729,11 @@ { "cell_type": "code", "execution_count": null, + "id": "e390be8a", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class ExceptionExpected:\n", " \"Context manager that tests if an exception is raised\"\n", " def __init__(self, ex=Exception, regex=''): self.ex,self.regex = ex,regex\n", @@ -697,6 +747,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a700f3b1", "metadata": {}, "outputs": [], "source": [ @@ -710,6 +761,7 @@ }, { "cell_type": "markdown", + "id": "10baa17d", "metadata": {}, "source": [ "`exception` is an abbreviation for `ExceptionExpected()`." @@ -718,16 +770,18 @@ { "cell_type": "code", "execution_count": null, + "id": "f16224d6", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "exception = ExceptionExpected()" ] }, { "cell_type": "code", "execution_count": null, + "id": "9310c088", "metadata": {}, "outputs": [], "source": [ @@ -737,6 +791,7 @@ { "cell_type": "code", "execution_count": null, + "id": "29279e9e", "metadata": {}, "outputs": [], "source": [ @@ -756,6 +811,7 @@ }, { "cell_type": "markdown", + "id": "ed04dff5", "metadata": {}, "source": [ "## Export -" @@ -764,6 +820,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0dd98654", "metadata": {}, "outputs": [], "source": [ @@ -776,21 +833,13 @@ { "cell_type": "code", "execution_count": null, + "id": "3f8b1eca", "metadata": {}, "outputs": [], "source": [] } ], - "metadata": { - "jupytext": { - "split_at_heading": true - }, - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, + "metadata": {}, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } From e08ab551a4d948d6f7fe50c4bada8f87528ae3f8 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Thu, 24 Jul 2025 09:03:30 +1000 Subject: [PATCH 102/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3214fb4..602140c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.8.6 + +### New Features + +- Add `exc` param to `test_fail` ([#690](https://github.com/AnswerDotAI/fastcore/issues/690)) + + ## 1.8.5 ### New Features From 512bd04c43f22941b8195fba47593095c99a06b8 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Thu, 24 Jul 2025 09:03:42 +1000 Subject: [PATCH 103/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 17ecd62a..f7d787dc 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.6" +__version__ = "1.8.7" diff --git a/settings.ini b/settings.ini index c1fc8bd0..16a6c4db 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.6 +version = 1.8.7 min_python = 3.9 audience = Developers language = English From 2383daae7a4bf5198d245b82f4360a91db01a1cd Mon Sep 17 00:00:00 2001 From: Rens Date: Thu, 24 Jul 2025 12:53:15 +0200 Subject: [PATCH 104/182] add uid and gid to mk_write --- fastcore/xtras.py | 3 ++- nbs/03_xtras.ipynb | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 79371999..e3bf88ab 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -338,10 +338,11 @@ def read_json(self:Path, encoding=None, errors=None): # %% ../nbs/03_xtras.ipynb @patch -def mk_write(self:Path, data, encoding=None, errors=None, mode=511): +def mk_write(self:Path, data, encoding=None, errors=None, mode=511, uid=-1, gid=-1): "Make all parent dirs of `self`, and write `data`" self.parent.mkdir(exist_ok=True, parents=True, mode=mode) self.write_text(data, encoding=encoding, errors=errors) + if uid!=-1 or gid!=-1: os.chown(self, uid, gid) # %% ../nbs/03_xtras.ipynb @patch diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 5c643c20..3fc3cd8b 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -1167,10 +1167,11 @@ "source": [ "#|export\n", "@patch\n", - "def mk_write(self:Path, data, encoding=None, errors=None, mode=511):\n", + "def mk_write(self:Path, data, encoding=None, errors=None, mode=511, uid=-1, gid=-1):\n", " \"Make all parent dirs of `self`, and write `data`\"\n", " self.parent.mkdir(exist_ok=True, parents=True, mode=mode)\n", - " self.write_text(data, encoding=encoding, errors=errors)" + " self.write_text(data, encoding=encoding, errors=errors)\n", + " if uid!=-1 or gid!=-1: os.chown(self, uid, gid)" ] }, { From ba3ce420346854167422e94712b0782ce55077e3 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 26 Jul 2025 01:57:38 +1000 Subject: [PATCH 105/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 602140c9..18167de0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.8.7 + +### New Features + +- Add uid and gid to `mk_write` as optional parameters ([#691](https://github.com/AnswerDotAI/fastcore/pull/691)), thanks to [@RensDimmendaal](https://github.com/RensDimmendaal) + + ## 1.8.6 ### New Features From da566b20c530666791422cf541a537b9e0d83e34 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 26 Jul 2025 01:57:52 +1000 Subject: [PATCH 106/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index f7d787dc..cd12fb00 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.7" +__version__ = "1.8.8" diff --git a/settings.ini b/settings.ini index 16a6c4db..d0946324 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.7 +version = 1.8.8 min_python = 3.9 audience = Developers language = English From 1388c91e062c2479c6631d1c7908d51d99432f98 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 24 Aug 2025 08:34:43 +1000 Subject: [PATCH 107/182] fixes #692 --- fastcore/_modidx.py | 1 + fastcore/xtras.py | 15 ++++-- nbs/03_xtras.ipynb | 121 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 111 insertions(+), 26 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 85ab1938..d24fbc5d 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -655,4 +655,5 @@ 'fastcore.xtras.type2str': ('xtras.html#type2str', 'fastcore/xtras.py'), 'fastcore.xtras.untar_dir': ('xtras.html#untar_dir', 'fastcore/xtras.py'), 'fastcore.xtras.utc2local': ('xtras.html#utc2local', 'fastcore/xtras.py'), + 'fastcore.xtras.vars_pub': ('xtras.html#vars_pub', 'fastcore/xtras.py'), 'fastcore.xtras.walk': ('xtras.html#walk', 'fastcore/xtras.py')}}} diff --git a/fastcore/xtras.py b/fastcore/xtras.py index e3bf88ab..8b1ca42d 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -13,7 +13,7 @@ 'truncstr', 'sparkline', 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', - 'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', 'flexiclass', 'asdict', 'is_typeddict', + 'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict', 'is_namedtuple', 'CachedIter', 'CachedAwaitable', 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] @@ -770,9 +770,16 @@ def asdict(o)->dict: elif hasattr(o, '__iter__'): try: r = dict(o) except TypeError: pass - elif hasattr(o, '__dict__'): r = o.__dict__ - else: raise TypeError(f'Can not convert {o} to a dict') - return {k:v for k,v in r.items() if v not in (UNSET,MISSING)} + elif hasattr(o, '__flds__'): r = {x:getattr(o,x) for x in o.__flds__} + else: r = vars(o) + skip = set(getattr(o, '__skip__', [])) + return {k:v for k,v in r.items() if v is not UNSET and v is not MISSING and k not in skip} + +# %% ../nbs/03_xtras.ipynb +def vars_pub(x): + "Get public non-skipped vars" + skip = set(getattr(x, '__skip__', [])) + return [o for o in vars(x) if o[0]!='_' and o not in skip] # %% ../nbs/03_xtras.ipynb def is_typeddict(cls:type)->bool: diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 3fc3cd8b..6d5acd77 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -688,7 +688,7 @@ { "data": { "text/plain": [ - "'pip 25.1.1 from /Users/jhoward/aai-ws/.venv/lib/python3.12/site-packages/pip (python 3.12)'" + "'pip 25.2 from /Users/jhoward/aai-ws/.venv/lib/python3.12/site-packages/pip (python 3.12)'" ] }, "execution_count": null, @@ -1441,7 +1441,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L389){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L391){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1452,7 +1452,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L389){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L391){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1524,7 +1524,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1535,7 +1535,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L400){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1624,7 +1624,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L404){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1635,7 +1635,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L404){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1689,7 +1689,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L403){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -1700,7 +1700,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L401){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L403){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -1733,7 +1733,7 @@ { "data": { "text/plain": [ - "['g', 'c', 'f', 'd', 'a', 'b', 'e', 'h']" + "['f', 'b', 'd', 'e', 'g', 'a', 'c', 'h']" ] }, "execution_count": null, @@ -2274,7 +2274,7 @@ { "data": { "text/plain": [ - "'https://github.com/fastai/fastcore/tree/master/fastcore/test.py#L37'" + "'https://github.com/fastai/fastcore/tree/master/fastcore/test.py#L38'" ] }, "execution_count": null, @@ -2577,7 +2577,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L532){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L573){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### EventTimer\n", "\n", @@ -2588,7 +2588,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L532){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L573){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### EventTimer\n", "\n", @@ -2622,8 +2622,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Num Events: 13, Freq/sec: 451.7\n", - "Most recent: ▁▁▁▂▇ 264.5 257.0 278.7 293.7 363.0\n" + "Num Events: 1, Freq/sec: 161.2\n", + "Most recent: ▁▇▆▁▃ 59.3 132.1 120.0 58.9 92.0\n" ] } ], @@ -2702,7 +2702,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L564){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L605){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### PartialFormatter\n", "\n", @@ -2713,7 +2713,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L564){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L605){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### PartialFormatter\n", "\n", @@ -2918,7 +2918,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L621){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L662){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ContextManagers\n", "\n", @@ -2929,7 +2929,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L621){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L662){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ContextManagers\n", "\n", @@ -3362,9 +3362,10 @@ " elif hasattr(o, '__iter__'):\n", " try: r = dict(o)\n", " except TypeError: pass\n", - " elif hasattr(o, '__dict__'): r = o.__dict__\n", - " else: raise TypeError(f'Can not convert {o} to a dict')\n", - " return {k:v for k,v in r.items() if v not in (UNSET,MISSING)}" + " elif hasattr(o, '__flds__'): r = {x:getattr(o,x) for x in o.__flds__}\n", + " else: r = vars(o)\n", + " skip = set(getattr(o, '__skip__', []))\n", + " return {k:v for k,v in r.items() if v is not UNSET and v is not MISSING and k not in skip}" ] }, { @@ -3394,6 +3395,28 @@ "asdict(bob)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Set the optional `__flds__` parameter to customise the field list, and the optional `__skip__` parameter to skip some names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class CustomObj:\n", + " def __init__(self): self.a,self.b,self.c,self.d = 1,2,3,4\n", + " __flds__ = ['a','b','c','d']\n", + " __skip__ = ['b']\n", + "\n", + "obj = CustomObj()\n", + "test_eq(asdict(obj), {'a': 1, 'c': 3, 'd': 4})" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -3401,6 +3424,60 @@ "To customise dict conversion behavior for a class, implement the `_asdict` method (this is used in the Python stdlib for named tuples)." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def vars_pub(x):\n", + " \"Get public non-skipped vars\"\n", + " skip = set(getattr(x, '__skip__', []))\n", + " return [o for o in vars(x) if o[0]!='_' and o not in skip]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `vars_pub` function returns a list of public (non-underscore-prefixed) variable names from an object, excluding any names listed in the object's optional `__skip__` attribute." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class TestObj:\n", + " def __init__(self): self.pub_attr,self._priv_attr,self.another_pub,self.skip_me = 1,2,3,4\n", + " __skip__ = ['skip_me']\n", + "\n", + "obj = TestObj()\n", + "test_eq(vars_pub(obj), ['pub_attr', 'another_pub'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Without `__skip__`, all pub vars are returned" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class SimpleObj:\n", + " def __init__(self): self.a,self._b,self.c = 1,2,3\n", + "\n", + "simple = SimpleObj()\n", + "test_eq(vars_pub(simple), ['a', 'c'])" + ] + }, { "cell_type": "code", "execution_count": null, From 04a28fca7665083fdbd74862caa7d83d6926e4be Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 24 Aug 2025 08:35:44 +1000 Subject: [PATCH 108/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18167de0..be7c7eff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.8.8 + +### New Features + +- Add `vars_pub` ([#692](https://github.com/AnswerDotAI/fastcore/issues/692)) +- Add `__skipped__` and `__flds__` to `asdict` ([#692](https://github.com/AnswerDotAI/fastcore/issues/692)) + + ## 1.8.7 ### New Features From 7f175fadd15dd9d0a3dd58535f05095c8bdd8dfd Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 24 Aug 2025 08:35:58 +1000 Subject: [PATCH 109/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index cd12fb00..490fbd4b 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.8" +__version__ = "1.8.9" diff --git a/settings.ini b/settings.ini index d0946324..26894b7f 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.8 +version = 1.8.9 min_python = 3.9 audience = Developers language = English From 0619fb1f8fe04475c4e9cf448e87497826e290a3 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 22 Sep 2025 10:37:21 +1000 Subject: [PATCH 110/182] fixes #693 --- fastcore/ansi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fastcore/ansi.py b/fastcore/ansi.py index c921ee35..cc5e46c8 100644 --- a/fastcore/ansi.py +++ b/fastcore/ansi.py @@ -12,6 +12,12 @@ _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_terminal_queries(text): + # Remove OSC sequences (like background color queries) + text = re.sub(r'\x1b\][^\\x07]*\x07', '', text) + # Remove DSR sequences (device status reports) + return re.sub(r'\x1b\[[0-9]*n', '', text) + def strip_ansi(source): "Remove ANSI escape codes from text." @@ -20,7 +26,7 @@ def strip_ansi(source): def ansi2html(text): "Convert ANSI colors to HTML colors." - text = escape(text) + text = escape(strip_terminal_queries(text)) return _ansi2anything(text, _htmlconverter) From 12639d498564404dd37a3b6168d252a1f83dd755 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 22 Sep 2025 10:37:40 +1000 Subject: [PATCH 111/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be7c7eff..4331a3b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.8.9 + +### New Features + +- Strip terminal queries in fastcore.ansi ([#693](https://github.com/AnswerDotAI/fastcore/issues/693)) + + ## 1.8.8 ### New Features From 83a810b30cb784b37196487c6a301ab68d4a5f43 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 22 Sep 2025 10:38:04 +1000 Subject: [PATCH 112/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 490fbd4b..d3d41cec 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.9" +__version__ = "1.8.10" diff --git a/settings.ini b/settings.ini index 26894b7f..6c55a007 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.9 +version = 1.8.10 min_python = 3.9 audience = Developers language = English From d2f315d3f56c4f38e018932face645e455f11dac Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 23 Sep 2025 11:46:53 +1000 Subject: [PATCH 113/182] fixes #695 --- fastcore/_modidx.py | 9 + fastcore/tools.py | 146 +++++++++ nbs/12_tools.ipynb | 754 ++++++++++++++++++++++++++++++++++++++++++++ settings.ini | 2 +- 4 files changed, 910 insertions(+), 1 deletion(-) create mode 100644 fastcore/tools.py create mode 100644 nbs/12_tools.ipynb diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index d24fbc5d..57df8616 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -491,6 +491,15 @@ 'fastcore.test.test_shuffled': ('test.html#test_shuffled', 'fastcore/test.py'), 'fastcore.test.test_stdout': ('test.html#test_stdout', 'fastcore/test.py'), 'fastcore.test.test_warns': ('test.html#test_warns', 'fastcore/test.py')}, + 'fastcore.tools': { 'fastcore.tools.create': ('tools.html#create', 'fastcore/tools.py'), + 'fastcore.tools.insert': ('tools.html#insert', 'fastcore/tools.py'), + 'fastcore.tools.replace_lines': ('tools.html#replace_lines', 'fastcore/tools.py'), + 'fastcore.tools.rg': ('tools.html#rg', 'fastcore/tools.py'), + 'fastcore.tools.run_cmd': ('tools.html#run_cmd', 'fastcore/tools.py'), + 'fastcore.tools.sed': ('tools.html#sed', 'fastcore/tools.py'), + 'fastcore.tools.str_replace': ('tools.html#str_replace', 'fastcore/tools.py'), + 'fastcore.tools.strs_replace': ('tools.html#strs_replace', 'fastcore/tools.py'), + 'fastcore.tools.view': ('tools.html#view', 'fastcore/tools.py')}, 'fastcore.transform': {}, 'fastcore.utils': {}, 'fastcore.xdg': { 'fastcore.xdg._path_from_env': ('xdg.html#_path_from_env', 'fastcore/xdg.py'), diff --git a/fastcore/tools.py b/fastcore/tools.py new file mode 100644 index 00000000..a70caf9f --- /dev/null +++ b/fastcore/tools.py @@ -0,0 +1,146 @@ +"""Helpful tools for running cli commands and reading, modifying, and creating files in python. This is used primarily for AI's in tool loops for automating tasks involving the filesystem.""" + +# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/12_tools.ipynb. + +# %% auto 0 +__all__ = ['run_cmd', 'rg', 'sed', 'view', 'create', 'insert', 'str_replace', 'strs_replace', 'replace_lines'] + +# %% ../nbs/12_tools.ipynb +def run_cmd( + cmd:str, # The command name to run + argstr:str='', # All args to the command, will be split with shlex + disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command + allow_re:str=None # optional regex which, if not matched on argstr, will disallow the command +): + "Run `cmd` passing split `argstr`, optionally checking for allowed argstr" + if disallow_re and re.search(disallow_re, argstr): return 'Error: args disallowed' + if allow_re and re.search( allow_re, argstr): return 'Error: args not allowed' + try: outp = run([cmd] + split(argstr), text=True, stdin=DEVNULL, capture_output=True) + except Exception as e: return f'Error running cmd: {str(e)}' + res = outp.stdout + if res and outp.stderr: res += '\n' + return res + outp.stderr + +# %% ../nbs/12_tools.ipynb +def rg( + argstr:str, # All args to the command, will be split with shlex + disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command + allow_re:str=None # optional regex which, if not matched on argstr, will disallow the command +): + "Run the `rg` command with the args in `argstr` (no need to backslash escape)" + return run_cmd('rg', '-n '+argstr, disallow_re=disallow_re, allow_re=allow_re) + +# %% ../nbs/12_tools.ipynb +def sed( + argstr:str, # All args to the command, will be split with shlex + disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command + allow_re:str=None # optional regex which, if not matched on argstr, will disallow the command +): + "Run the `sed` command with the args in `argstr` (e.g for reading a section of a file)" + return run_cmd('sed', argstr, allow_re=allow_re, disallow_re=disallow_re) + +# %% ../nbs/12_tools.ipynb +def view( + path:str, # Path to directory or file to view + view_range:tuple[int,int]=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF + nums:bool=False # Whether to show line numbers +): + 'View directory or file contents with optional line range and numbers' + try: + p = Path(path).expanduser().resolve() + if not p.exists(): return f'Error: File not found: {p}' + header = None + if p.is_dir(): + files = [str(f) for f in p.glob('**/*') + if not any(part.startswith('.') for part in f.relative_to(p).parts)] + lines = files + header = f'Directory contents of {p}:' + else: lines = p.read_text().splitlines() + s, e = 1, len(lines) + if view_range: + s,e = view_range + if not (1<=s<=len(lines)): return f'Error: Invalid start line {s}' + if e!=-1 and not (s<=e<= len(lines)): return f'Error: Invalid end line {e}' + lines = lines[s-1:None if e==-1 else e] + if nums: lines = [f'{i+s:6d} │ {l}' for i, l in enumerate(lines)] + content = '\n'.join(lines) + return f'{header}\n{content}' if header else content + except Exception as e: return f'Error viewing: {str(e)}' + +# %% ../nbs/12_tools.ipynb +def create( + path: str, # Path where the new file should be created + file_text: str, # Content to write to the file + overwrite:bool=False # Whether to overwrite existing files +) -> str: + 'Creates a new file with the given content at the specified path' + try: + p = Path(path) + if p.exists(): + if not overwrite: return f'Error: File already exists: {p}' + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(file_text) + return f'Created file {p}.' + except Exception as e: return f'Error creating file: {str(e)}' + +# %% ../nbs/12_tools.ipynb +def insert( + path: str, # Path to the file to modify + insert_line: int, # Line number where to insert (0-based indexing) + new_str: str # Text to insert at the specified line +) -> str: + 'Insert new_str at specified line number' + try: + p = Path(path) + if not p.exists(): return f'Error: File not found: {p}' + content = p.read_text().splitlines() + if not (0 <= insert_line <= len(content)): return f'Error: Invalid line number {insert_line}' + content.insert(insert_line, new_str) + new_content = '\n'.join(content) + p.write_text(new_content) + return f'Inserted text at line {insert_line} in {p}' + except Exception as e: return f'Error inserting text: {str(e)}' + +# %% ../nbs/12_tools.ipynb +def str_replace( + path: str, # Path to the file to modify + old_str: str, # Text to find and replace + new_str: str # Text to replace with +) -> str: + 'Replace first occurrence of old_str with new_str in file' + try: + p = Path(path) + if not p.exists(): return f'Error: File not found: {p}' + content = p.read_text() + count = content.count(old_str) + if count == 0: return 'Error: Text not found in file' + if count > 1: return f'Error: Multiple matches found ({count})' + new_content = content.replace(old_str, new_str, 1) + p.write_text(new_content) + return f'Replaced text in {p}' + except Exception as e: return f'Error replacing text: {str(e)}' + +# %% ../nbs/12_tools.ipynb +def strs_replace( + path:str, # Path to the file to modify + old_strs:list[str], # List of strings to find and replace + new_strs:list[str], # List of replacement strings (must match length of old_strs) +): + "Replace for each str pair in old_strs,new_strs call `str_replace" + res = [str_replace(path, old, new) for (old,new) in zip(old_strs,new_strs)] + return 'Results for each replacement:\n' + '; '.join(res) + +# %% ../nbs/12_tools.ipynb +def replace_lines( + path:str, # Path to the file to modify + start_line:int, # Starting line number to replace (1-based indexing) + end_line:int, # Ending line number to replace (1-based indexing, inclusive) + new_content:str, # New content to replace the specified lines +): + "Replace lines in file using start and end line-numbers (index starting at 1)" + if not (p := Path(path)).exists(): return f"Error: File not found: {p}" + content = p.readlines() + if not new_content.endswith('\n'): new_content+='\n' + content[start_line-1:end_line] = [new_content] + p.write_text(''.join(content)) + return f"Replaced lines {start_line} to {end_line}." diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb new file mode 100644 index 00000000..a6ac77ba --- /dev/null +++ b/nbs/12_tools.ipynb @@ -0,0 +1,754 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "e23f3fbd", + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp tools" + ] + }, + { + "cell_type": "markdown", + "id": "5e356282", + "metadata": {}, + "source": [ + "# LLM tools\n", + "> Helpful tools for running cli commands and reading, modifying, and creating files in python. This is used primarily for AI's in tool loops for automating tasks involving the filesystem." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "578246d2", + "metadata": {}, + "outputs": [], + "source": [ + "from fastcore.utils import *\n", + "from shlex import split\n", + "from subprocess import run, DEVNULL" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "490fb7ce", + "metadata": {}, + "outputs": [], + "source": [ + "from fastcore.test import test_eq\n", + "from toolslm.funccall import get_schema\n", + "import inspect" + ] + }, + { + "cell_type": "markdown", + "id": "55a65ce3", + "metadata": {}, + "source": [ + "## Bash Tools" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1182336d", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def run_cmd(\n", + " cmd:str, # The command name to run\n", + " argstr:str='', # All args to the command, will be split with shlex\n", + " disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command\n", + " allow_re:str=None # optional regex which, if not matched on argstr, will disallow the command\n", + "):\n", + " \"Run `cmd` passing split `argstr`, optionally checking for allowed argstr\"\n", + " if disallow_re and re.search(disallow_re, argstr): return 'Error: args disallowed'\n", + " if allow_re and re.search( allow_re, argstr): return 'Error: args not allowed'\n", + " try: outp = run([cmd] + split(argstr), text=True, stdin=DEVNULL, capture_output=True)\n", + " except Exception as e: return f'Error running cmd: {str(e)}'\n", + " res = outp.stdout\n", + " if res and outp.stderr: res += '\\n'\n", + " return res + outp.stderr" + ] + }, + { + "cell_type": "markdown", + "id": "8a7d1ffc", + "metadata": {}, + "source": [ + "With this little function, we can now run any cli command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a6f8256", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "000_tour.ipynb\n", + "00_test.ipynb\n", + "01_basics.ipynb\n", + "02_foundation.ipynb\n", + "03_xtras.ipynb\n", + "03a_parallel.ipynb\n", + "03b_net.ipynb\n", + "04_docments.ipy\n" + ] + } + ], + "source": [ + "print(run_cmd('ls')[:128])" + ] + }, + { + "cell_type": "markdown", + "id": "ee40009c", + "metadata": {}, + "source": [ + "Note that, for tool safety, this is not passed through the shell, so wildcards, env vars, etc will not work:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04991f6f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ls: f*: No such file or directory\n", + "\n" + ] + } + ], + "source": [ + "print(run_cmd('ls', 'f*')[:128])" + ] + }, + { + "cell_type": "markdown", + "id": "c98af7cc", + "metadata": {}, + "source": [ + "Let's create some useful functions from this that will allow for searching, reading and modifing content on the file system." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb253a39", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def rg(\n", + " argstr:str, # All args to the command, will be split with shlex\n", + " disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command\n", + " allow_re:str=None # optional regex which, if not matched on argstr, will disallow the command\n", + "):\n", + " \"Run the `rg` command with the args in `argstr` (no need to backslash escape)\"\n", + " return run_cmd('rg', '-n '+argstr, disallow_re=disallow_re, allow_re=allow_re)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e26cdce2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1:fastcore.fast.ai\\n'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rg('fast.ai CNAME')" + ] + }, + { + "cell_type": "markdown", + "id": "3222e44a", + "metadata": {}, + "source": [ + "Functions implemented with `run_cmd` like this one can be passed regexps to allow or disallow arg strs, i.e to block parent or root directories:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0852c259", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Error: args disallowed'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "disallowed = r' /|\\.\\.'\n", + "rg('info@fast.ai ..', disallow_re=disallowed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b27448c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Error: args disallowed'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rg('info@fast.ai /', disallow_re=disallowed)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2580269c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1:fastcore.fast.ai\n", + "\n" + ] + } + ], + "source": [ + "print(rg('fast.ai CNAME', disallow_re=disallowed))" + ] + }, + { + "cell_type": "markdown", + "id": "345a22df", + "metadata": {}, + "source": [ + "NB: These tools have special behavior around errors. Since these have been speficially designed for work with LLMs, any exceptions created from there use is returned as a string to help them debug their work." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "472ed6a5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Error running cmd: [Errno 2] No such file or directory: 'asdfe'\"" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run_cmd('asdfe')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "09de7b32", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def sed(\n", + " argstr:str, # All args to the command, will be split with shlex\n", + " disallow_re:str=None, # optional regex which, if matched on argstr, will disallow the command\n", + " allow_re:str=None # optional regex which, if not matched on argstr, will disallow the command\n", + "):\n", + " \"Run the `sed` command with the args in `argstr` (e.g for reading a section of a file)\"\n", + " return run_cmd('sed', argstr, allow_re=allow_re, disallow_re=disallow_re)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64b985df", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project:\n", + " type: website\n", + " pre-render: \n", + " - pysym2md --output_file apilist.txt fastcore\n", + " post-render: \n", + "\n" + ] + } + ], + "source": [ + "print(sed('-n \"1,5 p\" _quarto.yml'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de648f3e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1\n", + "project:\n", + "2\n", + " type: website\n", + "3\n", + " pre-render: \n", + "4\n", + " - pysym2md --output_file apilist.txt fastcore\n", + "5\n", + " post-render: \n", + "\n" + ] + } + ], + "source": [ + "# Print line numbers too\n", + "print(sed('-n \"1,5 {=;p;}\" _quarto.yml'))" + ] + }, + { + "cell_type": "markdown", + "id": "290b38eb", + "metadata": {}, + "source": [ + "## Text Edit Tools" + ] + }, + { + "cell_type": "markdown", + "id": "d3cd7681", + "metadata": {}, + "source": [ + "Python implementations of the text editor tools from [Anthropic](https://docs.claude.com/en/docs/agents-and-tools/tool-use/text-editor-tool). These tools are especially useful in an AI's tool loop. See [`claudette`](https://claudette.answer.ai/text_editor.html) for examples." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5dd1aaf3", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def view(\n", + " path:str, # Path to directory or file to view\n", + " view_range:tuple[int,int]=None, # Optional 1-indexed (start, end) line range for files, end=-1 for EOF\n", + " nums:bool=False # Whether to show line numbers\n", + "):\n", + " 'View directory or file contents with optional line range and numbers'\n", + " try:\n", + " p = Path(path).expanduser().resolve()\n", + " if not p.exists(): return f'Error: File not found: {p}'\n", + " header = None\n", + " if p.is_dir():\n", + " files = [str(f) for f in p.glob('**/*') \n", + " if not any(part.startswith('.') for part in f.relative_to(p).parts)]\n", + " lines = files\n", + " header = f'Directory contents of {p}:'\n", + " else: lines = p.read_text().splitlines()\n", + " s, e = 1, len(lines)\n", + " if view_range:\n", + " s,e = view_range\n", + " if not (1<=s<=len(lines)): return f'Error: Invalid start line {s}'\n", + " if e!=-1 and not (s<=e<= len(lines)): return f'Error: Invalid end line {e}'\n", + " lines = lines[s-1:None if e==-1 else e]\n", + " if nums: lines = [f'{i+s:6d} │ {l}' for i, l in enumerate(lines)]\n", + " content = '\\n'.join(lines)\n", + " return f'{header}\\n{content}' if header else content\n", + " except Exception as e: return f'Error viewing: {str(e)}'" + ] + }, + { + "cell_type": "markdown", + "id": "99a1f0bb", + "metadata": {}, + "source": [ + "You can specify line ranges and whether to have the output contain line numbers:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7549c8bd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 1 │ project:\n", + " 2 │ type: website\n", + " 3 │ pre-render: \n", + " 4 │ - pysym2md --output_file apilist.txt fastcore\n", + " 5 │ post-render: \n", + " 6 │ - llms_txt2ctx llms.txt --optional true --save_nbdev_fname llms-ctx-full.txt\n", + " 7 │ - llms_txt2ctx llms.txt --save_nbdev_fname llms-ctx.txt\n", + " 8 │ resources: \n", + " 9 │ - \"*.txt\"\n", + " 10 │ preview:\n" + ] + } + ], + "source": [ + "print(view('_quarto.yml', (1,10), nums=True))" + ] + }, + { + "cell_type": "markdown", + "id": "4b794ca3", + "metadata": {}, + "source": [ + "Here's what the output looks like when viewing a directory:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "047970e0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Directory contents of /Users/jhoward/aai-ws/fastcore/nbs:\n", + "/Users/jhoward/aai-ws/fastcore/nbs/llms.txt\n", + "/Users/jhoward/aai-ws/fastcore/nbs/000_tour.ipynb\n", + "/Users/jhoward/aai-ws/fastcore/nbs/parallel_test.py\n", + "/Users/jhoward/aai-ws/fastcore/nbs/_quarto.yml\n", + "/Users/jhoward/aai-ws/fastcore/nbs/08_style.ipynb\n" + ] + } + ], + "source": [ + "print(view('.', (1,5)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36f58e38", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def create(\n", + " path: str, # Path where the new file should be created\n", + " file_text: str, # Content to write to the file\n", + " overwrite:bool=False # Whether to overwrite existing files\n", + ") -> str:\n", + " 'Creates a new file with the given content at the specified path'\n", + " try:\n", + " p = Path(path)\n", + " if p.exists():\n", + " if not overwrite: return f'Error: File already exists: {p}'\n", + " p.parent.mkdir(parents=True, exist_ok=True)\n", + " p.write_text(file_text)\n", + " return f'Created file {p}.'\n", + " except Exception as e: return f'Error creating file: {str(e)}'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dae94280", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created file test.txt.\n", + "Contents:\n", + " 1 │ Hello, world!\n" + ] + } + ], + "source": [ + "print(create('test.txt', 'Hello, world!'))\n", + "f = Path('test.txt')\n", + "test_eq(f.exists(), True)\n", + "print('Contents:\\n', view(f, nums=True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "434147ef", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def insert(\n", + " path: str, # Path to the file to modify\n", + " insert_line: int, # Line number where to insert (0-based indexing)\n", + " new_str: str # Text to insert at the specified line\n", + ") -> str:\n", + " 'Insert new_str at specified line number'\n", + " try:\n", + " p = Path(path)\n", + " if not p.exists(): return f'Error: File not found: {p}'\n", + " content = p.read_text().splitlines()\n", + " if not (0 <= insert_line <= len(content)): return f'Error: Invalid line number {insert_line}'\n", + " content.insert(insert_line, new_str)\n", + " new_content = '\\n'.join(content)\n", + " p.write_text(new_content)\n", + " return f'Inserted text at line {insert_line} in {p}'\n", + " except Exception as e: return f'Error inserting text: {str(e)}'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6dc775d4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 1 │ Let's add a new line\n", + " 2 │ Hello, world!\n" + ] + } + ], + "source": [ + "insert(f, 0, 'Let\\'s add a new line')\n", + "print(view(f, nums=True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9272ff7d", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def str_replace(\n", + " path: str, # Path to the file to modify\n", + " old_str: str, # Text to find and replace\n", + " new_str: str # Text to replace with\n", + ") -> str:\n", + " 'Replace first occurrence of old_str with new_str in file'\n", + " try:\n", + " p = Path(path)\n", + " if not p.exists(): return f'Error: File not found: {p}'\n", + " content = p.read_text()\n", + " count = content.count(old_str)\n", + " if count == 0: return 'Error: Text not found in file'\n", + " if count > 1: return f'Error: Multiple matches found ({count})'\n", + " new_content = content.replace(old_str, new_str, 1)\n", + " p.write_text(new_content)\n", + " return f'Replaced text in {p}'\n", + " except Exception as e: return f'Error replacing text: {str(e)}'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e64d183b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 1 │ Let's add a \n", + " 2 │ Hello, world!\n" + ] + } + ], + "source": [ + "str_replace(f, 'new line', '')\n", + "print(view(f, nums=True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb907119", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def strs_replace(\n", + " path:str, # Path to the file to modify\n", + " old_strs:list[str], # List of strings to find and replace\n", + " new_strs:list[str], # List of replacement strings (must match length of old_strs)\n", + "):\n", + " \"Replace for each str pair in old_strs,new_strs call `str_replace\"\n", + " res = [str_replace(path, old, new) for (old,new) in zip(old_strs,new_strs)]\n", + " return 'Results for each replacement:\\n' + '; '.join(res)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a261608", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 1 │ Let's add a \n", + " 2 │ Hello, friends!\n", + " 3 │ Nice to see you.\n" + ] + } + ], + "source": [ + "strs_replace(f, [\"add a new line\", \"world!\"], [\"just say\", \"friends!\\nNice to see you.\"])\n", + "print(view(f, nums=True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94dd09ed", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def replace_lines(\n", + " path:str, # Path to the file to modify\n", + " start_line:int, # Starting line number to replace (1-based indexing)\n", + " end_line:int, # Ending line number to replace (1-based indexing, inclusive)\n", + " new_content:str, # New content to replace the specified lines\n", + "):\n", + " \"Replace lines in file using start and end line-numbers (index starting at 1)\"\n", + " if not (p := Path(path)).exists(): return f\"Error: File not found: {p}\"\n", + " content = p.readlines()\n", + " if not new_content.endswith('\\n'): new_content+='\\n'\n", + " content[start_line-1:end_line] = [new_content]\n", + " p.write_text(''.join(content))\n", + " return f\"Replaced lines {start_line} to {end_line}.\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "852bcce0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 1 │ Replaced first two lines\n", + " 2 │ Nice to see you.\n" + ] + } + ], + "source": [ + "replace_lines('test.txt', 1, 2, 'Replaced first two lines')\n", + "print(view('test.txt', nums=True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5a506495", + "metadata": {}, + "outputs": [], + "source": [ + "f.unlink()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4092b227", + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "for f,o in list(globals().items()):\n", + " if callable(o) and hasattr(o, '__module__') and o.__module__ == '__main__' and not f.startswith(('_', 'receive_')):\n", + " assert f == get_schema(globals()[f])['name']" + ] + }, + { + "cell_type": "markdown", + "id": "ed04dff5", + "metadata": {}, + "source": [ + "## Export -" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0dd98654", + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "#|eval: false\n", + "from nbdev import nbdev_export\n", + "nbdev_export()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "874f949c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/settings.ini b/settings.ini index 6c55a007..881aa537 100644 --- a/settings.ini +++ b/settings.ini @@ -17,7 +17,7 @@ license = apache2 status = 5 nbs_path = nbs doc_path = _docs -dev_requirements = numpy nbdev>=0.2.39 matplotlib pillow torch pandas nbclassic pysymbol_llm llms-txt plum-dispatch +dev_requirements = numpy nbdev>=0.2.39 matplotlib pillow torch pandas nbclassic pysymbol_llm llms-txt plum-dispatch toolslm git_url = https://github.com/AnswerDotAI/fastcore/ lib_path = fastcore title = fastcore From d62da2110bfc290b7372fe120453b448a7fb110e Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 23 Sep 2025 11:53:28 +1000 Subject: [PATCH 114/182] fixes #696 --- .github/workflows/main.yml | 2 +- pyproject.toml | 2 +- settings.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6cf14c67..9d687640 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos]#,windows] - py: ["3.9", "3.10", "3.11", "3.12"] + py: ["3.10", "3.11", "3.12", "3.13"] include: #- os: windows # shell: "/usr/bin/bash" diff --git a/pyproject.toml b/pyproject.toml index ef86dd25..a1a0e3c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name="fastcore" -requires-python=">=3.9" +requires-python=">=3.10" dynamic = [ "keywords", "description", "version", "dependencies", "optional-dependencies", "readme", "license", "authors", "classifiers", "entry-points", "scripts", "urls"] [tool.uv] diff --git a/settings.ini b/settings.ini index 881aa537..204d6613 100644 --- a/settings.ini +++ b/settings.ini @@ -9,7 +9,7 @@ author_email = infos@fast.ai copyright = fast.ai branch = main version = 1.8.10 -min_python = 3.9 +min_python = 3.10 audience = Developers language = English custom_sidebar = False From ae5448f39e004a3a0d85e73e25751330d0b2fc2e Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 23 Sep 2025 11:55:11 +1000 Subject: [PATCH 115/182] export --- fastcore/tools.py | 5 +++++ nbs/12_tools.ipynb | 2 ++ 2 files changed, 7 insertions(+) diff --git a/fastcore/tools.py b/fastcore/tools.py index a70caf9f..042e0c1e 100644 --- a/fastcore/tools.py +++ b/fastcore/tools.py @@ -5,6 +5,11 @@ # %% auto 0 __all__ = ['run_cmd', 'rg', 'sed', 'view', 'create', 'insert', 'str_replace', 'strs_replace', 'replace_lines'] +# %% ../nbs/12_tools.ipynb +from .utils import * +from shlex import split +from subprocess import run, DEVNULL + # %% ../nbs/12_tools.ipynb def run_cmd( cmd:str, # The command name to run diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index a6ac77ba..e6c4ab60 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -26,6 +26,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "from fastcore.utils import *\n", "from shlex import split\n", "from subprocess import run, DEVNULL" @@ -38,6 +39,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| hide\n", "from fastcore.test import test_eq\n", "from toolslm.funccall import get_schema\n", "import inspect" From ea076e89a4b3e44a83c83ef9e7659b04eaba333e Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 23 Sep 2025 11:56:29 +1000 Subject: [PATCH 116/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4331a3b7..0c38a3f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.8.10 + +### New Features + +- Require py 3.10 now that 3.9 is EOL ([#696](https://github.com/AnswerDotAI/fastcore/issues/696)) +- Add `fastcore.tools` ([#695](https://github.com/AnswerDotAI/fastcore/issues/695)) + + ## 1.8.9 ### New Features From 6840de09680472ef7f8952e4ba58852b2337a61c Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 23 Sep 2025 11:56:47 +1000 Subject: [PATCH 117/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index d3d41cec..d48d2bfa 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.10" +__version__ = "1.8.11" diff --git a/settings.ini b/settings.ini index 204d6613..31ef132c 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.10 +version = 1.8.11 min_python = 3.10 audience = Developers language = English From a940c01b9b8853f8ab99802cabf8216f08c472fe Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 23 Sep 2025 16:03:05 +1000 Subject: [PATCH 118/182] fixes #697 --- fastcore/tools.py | 2 +- fastcore/xml.py | 4 +++- fastcore/xtras.py | 9 ++++++--- nbs/03_xtras.ipynb | 29 ++++++++++++++++++++++++++--- nbs/09_xml.ipynb | 4 +++- nbs/12_tools.ipynb | 2 +- 6 files changed, 40 insertions(+), 10 deletions(-) diff --git a/fastcore/tools.py b/fastcore/tools.py index 042e0c1e..896bee7f 100644 --- a/fastcore/tools.py +++ b/fastcore/tools.py @@ -131,7 +131,7 @@ def strs_replace( old_strs:list[str], # List of strings to find and replace new_strs:list[str], # List of replacement strings (must match length of old_strs) ): - "Replace for each str pair in old_strs,new_strs call `str_replace" + "Replace for each str pair in old_strs,new_strs" res = [str_replace(path, old, new) for (old,new) in zip(old_strs,new_strs)] return 'Results for each replacement:\n' + '; '.join(res) diff --git a/fastcore/xml.py b/fastcore/xml.py index 7721a3d4..d4f913cb 100644 --- a/fastcore/xml.py +++ b/fastcore/xml.py @@ -136,7 +136,9 @@ class Safe(str): def __html__(self): return self # %% ../nbs/09_xml.ipynb -def _escape(s): return '' if s is None else s.__html__() if hasattr(s, '__html__') else escape(s) if isinstance(s, str) else s +def _escape(s): + return '' if s is None else s.__html__() if hasattr(s, '__html__') \ + else escape(s, quote=False) if isinstance(s, str) else s def _noescape(s): return '' if s is None else s.__html__() if hasattr(s, '__html__') else s # %% ../nbs/09_xml.ipynb diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 8b1ca42d..bff1d296 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -39,13 +39,15 @@ def walk( keep_folder:callable=ret_true, # function that returns True for folders to enter skip_folder:callable=ret_false, # function that returns True for folders to skip func:callable=os.path.join, # function to apply to each matched file - ret_folders:bool=False # return folders, not just files + ret_folders:bool=False, # return folders, not just files + sort:bool=True # sort files by name within each folder ): "Generator version of `os.walk`, using functions to filter files and folders" from copy import copy for root,dirs,files in os.walk(path, followlinks=symlinks): if keep_folder(root,''): if ret_folders: yield func(root, '') + if sort: files = sorted(files) yield from (func(root, name) for name in files if keep_file(root,name)) for name in copy(dirs): if skip_folder(root,name): dirs.remove(name) @@ -62,7 +64,8 @@ def globtastic( skip_file_re:str=None, # Skip files matching regex skip_folder_re:str=None, # Skip folders matching regex, func:callable=os.path.join, # function to apply to each matched file - ret_folders:bool=False # return folders, not just files + ret_folders:bool=False, # return folders, not just files + sort:bool=True # sort files by name within each folder )->L: # Paths to matched files "A more powerful `glob`, including regex matches, symlink handling, and skip parameters" from fnmatch import fnmatch @@ -79,7 +82,7 @@ def _keep_file(root, name): def _keep_folder(root, name): return not folder_re or folder_re.search(os.path.join(root,name)) def _skip_folder(root, name): return skip_folder_re and skip_folder_re.search(name) return L(walk(path, symlinks=symlinks, keep_file=_keep_file, keep_folder=_keep_folder, skip_folder=_skip_folder, - func=func, ret_folders=ret_folders)) + func=func, ret_folders=ret_folders, sort=sort)) # %% ../nbs/03_xtras.ipynb @contextmanager diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 6d5acd77..d0c2f560 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -93,13 +93,15 @@ " keep_folder:callable=ret_true, # function that returns True for folders to enter\n", " skip_folder:callable=ret_false, # function that returns True for folders to skip\n", " func:callable=os.path.join, # function to apply to each matched file\n", - " ret_folders:bool=False # return folders, not just files\n", + " ret_folders:bool=False, # return folders, not just files\n", + " sort:bool=True # sort files by name within each folder\n", "):\n", " \"Generator version of `os.walk`, using functions to filter files and folders\"\n", " from copy import copy\n", " for root,dirs,files in os.walk(path, followlinks=symlinks):\n", " if keep_folder(root,''):\n", " if ret_folders: yield func(root, '')\n", + " if sort: files = sorted(files)\n", " yield from (func(root, name) for name in files if keep_file(root,name))\n", " for name in copy(dirs):\n", " if skip_folder(root,name): dirs.remove(name)" @@ -123,7 +125,8 @@ " skip_file_re:str=None, # Skip files matching regex\n", " skip_folder_re:str=None, # Skip folders matching regex,\n", " func:callable=os.path.join, # function to apply to each matched file\n", - " ret_folders:bool=False # return folders, not just files\n", + " ret_folders:bool=False, # return folders, not just files\n", + " sort:bool=True # sort files by name within each folder\n", ")->L: # Paths to matched files\n", " \"A more powerful `glob`, including regex matches, symlink handling, and skip parameters\"\n", " from fnmatch import fnmatch\n", @@ -140,7 +143,7 @@ " def _keep_folder(root, name): return not folder_re or folder_re.search(os.path.join(root,name))\n", " def _skip_folder(root, name): return skip_folder_re and skip_folder_re.search(name)\n", " return L(walk(path, symlinks=symlinks, keep_file=_keep_file, keep_folder=_keep_folder, skip_folder=_skip_folder,\n", - " func=func, ret_folders=ret_folders))" + " func=func, ret_folders=ret_folders, sort=sort))" ] }, { @@ -163,6 +166,26 @@ "globtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c')" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(#5) ['./fastcore/basics.py','./fastcore/dispatch.py','./fastcore/docments.py','./fastcore/docscrape.py','./fastcore/script.py']" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "globtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c', sort=True)" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb index 0c9811d4..ca652e4a 100644 --- a/nbs/09_xml.ipynb +++ b/nbs/09_xml.ipynb @@ -364,7 +364,9 @@ "outputs": [], "source": [ "#| export\n", - "def _escape(s): return '' if s is None else s.__html__() if hasattr(s, '__html__') else escape(s) if isinstance(s, str) else s\n", + "def _escape(s):\n", + " return '' if s is None else s.__html__() if hasattr(s, '__html__') \\\n", + " else escape(s, quote=False) if isinstance(s, str) else s\n", "def _noescape(s): return '' if s is None else s.__html__() if hasattr(s, '__html__') else s" ] }, diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index e6c4ab60..c169b587 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -622,7 +622,7 @@ " old_strs:list[str], # List of strings to find and replace\n", " new_strs:list[str], # List of replacement strings (must match length of old_strs)\n", "):\n", - " \"Replace for each str pair in old_strs,new_strs call `str_replace\"\n", + " \"Replace for each str pair in old_strs,new_strs\"\n", " res = [str_replace(path, old, new) for (old,new) in zip(old_strs,new_strs)]\n", " return 'Results for each replacement:\\n' + '; '.join(res)" ] From 79a4c8143a81df69e3a63ed52d15d7fd598b2933 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 23 Sep 2025 16:03:21 +1000 Subject: [PATCH 119/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c38a3f4..69337298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.8.11 + +### New Features + +- Add sort param to globtastic ([#697](https://github.com/AnswerDotAI/fastcore/issues/697)) + + ## 1.8.10 ### New Features From d765970ee69c41539ed3626a9620e4c5fa0ca728 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Tue, 23 Sep 2025 16:06:01 +1000 Subject: [PATCH 120/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index d48d2bfa..0a400907 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.11" +__version__ = "1.8.12" diff --git a/settings.ini b/settings.ini index 31ef132c..a695f1b7 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.11 +version = 1.8.12 min_python = 3.10 audience = Developers language = English From 20524bb241013c90c1003df5ec027e381f3fdb88 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 29 Sep 2025 10:47:09 +1000 Subject: [PATCH 121/182] fixes #698 --- fastcore/_modidx.py | 3 + fastcore/foundation.py | 7 +- fastcore/xtras.py | 32 +++++-- nbs/02_foundation.ipynb | 72 ++++++++++++++++ nbs/03_xtras.ipynb | 181 +++++++++++++++++++++++++++++++++++++--- 5 files changed, 277 insertions(+), 18 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 57df8616..61361590 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -343,6 +343,7 @@ 'fastcore.foundation.is_bool': ('foundation.html#is_bool', 'fastcore/foundation.py'), 'fastcore.foundation.is_indexer': ('foundation.html#is_indexer', 'fastcore/foundation.py'), 'fastcore.foundation.mask2idxs': ('foundation.html#mask2idxs', 'fastcore/foundation.py'), + 'fastcore.foundation.product': ('foundation.html#product', 'fastcore/foundation.py'), 'fastcore.foundation.read_config_file': ('foundation.html#read_config_file', 'fastcore/foundation.py'), 'fastcore.foundation.save_config_file': ('foundation.html#save_config_file', 'fastcore/foundation.py'), 'fastcore.foundation.working_directory': ( 'foundation.html#working_directory', @@ -621,6 +622,7 @@ 'fastcore.xtras.expand_wildcards': ('xtras.html#expand_wildcards', 'fastcore/xtras.py'), 'fastcore.xtras.flexicache': ('xtras.html#flexicache', 'fastcore/xtras.py'), 'fastcore.xtras.flexiclass': ('xtras.html#flexiclass', 'fastcore/xtras.py'), + 'fastcore.xtras.friendly_name': ('xtras.html#friendly_name', 'fastcore/xtras.py'), 'fastcore.xtras.get_source_link': ('xtras.html#get_source_link', 'fastcore/xtras.py'), 'fastcore.xtras.globtastic': ('xtras.html#globtastic', 'fastcore/xtras.py'), 'fastcore.xtras.hl_md': ('xtras.html#hl_md', 'fastcore/xtras.py'), @@ -640,6 +642,7 @@ 'fastcore.xtras.modified_env': ('xtras.html#modified_env', 'fastcore/xtras.py'), 'fastcore.xtras.modify_exception': ('xtras.html#modify_exception', 'fastcore/xtras.py'), 'fastcore.xtras.mtime_policy': ('xtras.html#mtime_policy', 'fastcore/xtras.py'), + 'fastcore.xtras.n_friendly_names': ('xtras.html#n_friendly_names', 'fastcore/xtras.py'), 'fastcore.xtras.nullable_dc': ('xtras.html#nullable_dc', 'fastcore/xtras.py'), 'fastcore.xtras.obj2dict': ('xtras.html#obj2dict', 'fastcore/xtras.py'), 'fastcore.xtras.open_file': ('xtras.html#open_file', 'fastcore/xtras.py'), diff --git a/fastcore/foundation.py b/fastcore/foundation.py index c90a10a0..0ef7f7db 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -4,7 +4,7 @@ # %% auto 0 __all__ = ['working_directory', 'add_docs', 'docs', 'coll_repr', 'is_bool', 'mask2idxs', 'cycle', 'zip_cycle', 'is_indexer', - 'CollBase', 'L', 'save_config_file', 'read_config_file', 'Config'] + 'product', 'CollBase', 'L', 'save_config_file', 'read_config_file', 'Config'] # %% ../nbs/02_foundation.ipynb from .imports import * @@ -82,6 +82,11 @@ def is_indexer(idx): "Test whether `idx` will index a single item in a list" return isinstance(idx,int) or not getattr(idx,'ndim',1) +# %% ../nbs/02_foundation.ipynb +def product(xs): + "The product of elements of `xs`, with `None`s removed" + return reduce(operator.mul, [o for o in xs if o is not None], 1) + # %% ../nbs/02_foundation.ipynb class CollBase: "Base class for composing a list of `items`" diff --git a/fastcore/xtras.py b/fastcore/xtras.py index bff1d296..2ee18483 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -9,13 +9,13 @@ __all__ = ['spark_chars', 'UNSET', 'walk', 'globtastic', 'maybe_open', 'mkdir', 'image_size', 'bunzip', 'loads', 'loads_multi', 'dumps', 'untar_dir', 'repo_details', 'run', 'open_file', 'save_pickle', 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'mapped', 'IterLen', - 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', 'exec_eval', 'get_source_link', - 'truncstr', 'sparkline', 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', - 'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', - 'trace', 'modified_env', 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', - 'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict', - 'is_namedtuple', 'CachedIter', 'CachedAwaitable', 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', - 'timed_cache'] + 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', 'friendly_name', + 'n_friendly_names', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', 'modify_exception', + 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', + 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', 'ContextManagers', + 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', + 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict', 'is_namedtuple', 'CachedIter', 'CachedAwaitable', + 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] # %% ../nbs/03_xtras.ipynb from .imports import * @@ -453,6 +453,24 @@ def asave_iter(g): def _(*args, **kwargs): return _save_iter(g, *args, **kwargs) return _ +# %% ../nbs/03_xtras.ipynb +def friendly_name(levels=3, suffix=4): + "Generate a random human-readable name with customizable word levels and suffix length" + import random,string + adjectives = ['admiring', 'adoring', 'amazing', 'awesome', 'beautiful', 'blissful', 'bold', 'brave', 'busy', 'charming', 'clever', 'compassionate', 'confident', 'cool', 'dazzling', 'determined', 'dreamy', 'eager', 'ecstatic', 'elastic', 'elated', 'elegant', 'epic', 'exciting', 'fervent', 'festive', 'flamboyant', 'focused', 'friendly', 'frosty', 'funny', 'gallant', 'gifted', 'goofy', 'gracious', 'great', 'happy', 'hopeful', 'hungry', 'inspiring', 'intelligent', 'interesting', 'jolly', 'jovial', 'keen', 'kind', 'laughing', 'loving', 'lucid', 'magical', 'modest', 'nice', 'nifty', 'nostalgic', 'objective', 'optimistic', 'peaceful', 'pensive', 'practical', 'priceless', 'quirky', 'quizzical', 'relaxed', 'reverent', 'romantic', 'serene', 'sharp', 'silly', 'sleepy', 'stoic', 'sweet', 'tender', 'trusting', 'upbeat', 'vibrant', 'vigilant', 'vigorous', 'wizardly', 'wonderful', 'youthful', 'zealous', 'zen', 'golden', 'silver', 'crimson', 'azure', 'emerald', 'violet', 'amber', 'coral', 'turquoise', 'lavender', 'minty', 'citrus', 'vanilla', 'woody', 'floral', 'fresh', 'gentle', 'sparkling', 'precise', 'curious'] + nouns = ['tiger', 'eagle', 'river', 'mountain', 'forest', 'ocean', 'star', 'moon', 'wind', 'dragon', 'phoenix', 'wolf', 'bear', 'lion', 'shark', 'falcon', 'raven', 'crystal', 'diamond', 'ruby', 'sapphire', 'pearl', 'wave', 'tide', 'cloud', 'rainbow', 'sunset', 'sunrise', 'galaxy', 'comet', 'meteor', 'planet', 'nebula', 'cosmos', 'universe', 'atom', 'photon', 'quantum', 'matrix', 'cipher', 'code', 'signal', 'pulse', 'beam', 'ray', 'spark', 'frost', 'ice', 'snow', 'mist', 'fog', 'dew', 'rain', 'hail', 'helix', 'prism', 'lens', 'mirror', 'echo', 'heart', 'mind', 'dream', 'vision', 'hope', 'wish', 'magic', 'spell', 'charm', 'rune', 'symbol', 'token', 'key', 'door', 'gate', 'bridge', 'tower', 'castle', 'fortress', 'shield', 'dolphin', 'whale', 'penguin', 'butterfly', 'hummingbird', 'deer', 'rabbit', 'fox', 'otter', 'panda', 'koala', 'zebra', 'giraffe', 'elephant', 'valley', 'canyon', 'meadow', 'prairie', 'island', 'lake', 'pond', 'stream', 'waterfall', 'cliff', 'peak', 'hill', 'grove', 'garden', 'sunlight', 'breeze', 'melody', 'sparkle', 'whirlpool', 'windmill', 'carousel', 'spiral', 'glow'] + verbs = ['runs', 'flies', 'jumps', 'builds', 'creates', 'flows', 'shines', 'grows', 'moves', 'works', 'dances', 'sings', 'plays', 'dreams', 'thinks', 'learns', 'teaches', 'helps', 'heals', 'saves', 'protects', 'guards', 'watches', 'sees', 'hears', 'feels', 'knows', 'understands', 'discovers', 'explores', 'searches', 'finds', 'seeks', 'holds', 'carries', 'lifts', 'pushes', 'pulls', 'makes', 'crafts', 'forges', 'shapes', 'forms', 'molds', 'carves', 'joins', 'connects', 'links', 'binds', 'ties', 'opens', 'closes', 'starts', 'stops', 'begins', 'ends', 'finishes', 'completes', 'wins', 'triumphs', 'succeeds', 'achieves', 'accomplishes', 'reaches', 'arrives', 'departs', 'leaves', 'returns', 'comes', 'goes', 'travels', 'journeys', 'walks', 'sprints', 'races', 'speeds', 'rushes', 'hurries', 'waits', 'pauses', 'rests', 'sleeps', 'wakes', 'rises', 'climbs', 'ascends', 'descends', 'swims', 'dives', 'surfs', 'sails', 'paddles', 'hikes', 'treks', 'wanders', 'roams', 'ventures', 'navigates', 'glides', 'soars', 'floats', 'drifts', 'tosses', 'divides', 'shares', 'secures', 'settles', 'places', 'wonders', 'questions'] + adverbs = ['quickly', 'gracefully', 'gently', 'boldly', 'quietly', 'swiftly', 'carefully', 'eagerly', 'smoothly', 'brightly', 'softly', 'steadily', 'cleverly', 'proudly', 'calmly', 'freely', 'wisely', 'kindly', 'firmly', 'lightly', 'deeply', 'clearly', 'warmly', 'coolly', 'sharply', 'slowly', 'rapidly', 'silently', 'loudly', 'perfectly'] + suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=suffix)) + words = [random.choice(o) for o in (adjectives,nouns,verbs,adverbs)[:levels]] + return f"{'-'.join(words)}-{suffix}" + +# %% ../nbs/03_xtras.ipynb +def n_friendly_names(levels=3, suffix=4): + "Number of possible combos for `friendly_names" + ns = [102,116,110,30] + return product(ns[:levels])*36**suffix + # %% ../nbs/03_xtras.ipynb def exec_eval(code, # Code to exec/eval g=None, # Globals namespace dict diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index bd7488cb..5e64b896 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -464,6 +464,78 @@ "assert not is_indexer(np.array([[1, 2], [3, 4]]))" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def product(xs):\n", + " \"The product of elements of `xs`, with `None`s removed\"\n", + " return reduce(operator.mul, [o for o in xs if o is not None], 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "60" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product([None, 3, 4, 5])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product([])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum([])" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index d0c2f560..8d23dd9c 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -154,7 +154,7 @@ { "data": { "text/plain": [ - "(#5) ['./fastcore/docments.py','./fastcore/dispatch.py','./fastcore/basics.py','./fastcore/docscrape.py','./fastcore/script.py']" + "(#5) ['./fastcore/basics.py','./fastcore/dispatch.py','./fastcore/docments.py','./fastcore/docscrape.py','./fastcore/script.py']" ] }, "execution_count": null, @@ -1464,7 +1464,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L391){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L394){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1475,7 +1475,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L391){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L394){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1547,7 +1547,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L405){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1558,7 +1558,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L402){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L405){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1647,7 +1647,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L409){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1658,7 +1658,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L409){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1712,7 +1712,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L403){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -1723,7 +1723,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L403){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -1756,7 +1756,7 @@ { "data": { "text/plain": [ - "['f', 'b', 'd', 'e', 'g', 'a', 'c', 'h']" + "['d', 'c', 'a', 'b', 'e', 'h', 'g', 'f']" ] }, "execution_count": null, @@ -2094,6 +2094,167 @@ "## Other Helpers" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def friendly_name(levels=3, suffix=4):\n", + " \"Generate a random human-readable name with customizable word levels and suffix length\"\n", + " import random,string\n", + " adjectives = ['admiring', 'adoring', 'amazing', 'awesome', 'beautiful', 'blissful', 'bold', 'brave', 'busy', 'charming', 'clever', 'compassionate', 'confident', 'cool', 'dazzling', 'determined', 'dreamy', 'eager', 'ecstatic', 'elastic', 'elated', 'elegant', 'epic', 'exciting', 'fervent', 'festive', 'flamboyant', 'focused', 'friendly', 'frosty', 'funny', 'gallant', 'gifted', 'goofy', 'gracious', 'great', 'happy', 'hopeful', 'hungry', 'inspiring', 'intelligent', 'interesting', 'jolly', 'jovial', 'keen', 'kind', 'laughing', 'loving', 'lucid', 'magical', 'modest', 'nice', 'nifty', 'nostalgic', 'objective', 'optimistic', 'peaceful', 'pensive', 'practical', 'priceless', 'quirky', 'quizzical', 'relaxed', 'reverent', 'romantic', 'serene', 'sharp', 'silly', 'sleepy', 'stoic', 'sweet', 'tender', 'trusting', 'upbeat', 'vibrant', 'vigilant', 'vigorous', 'wizardly', 'wonderful', 'youthful', 'zealous', 'zen', 'golden', 'silver', 'crimson', 'azure', 'emerald', 'violet', 'amber', 'coral', 'turquoise', 'lavender', 'minty', 'citrus', 'vanilla', 'woody', 'floral', 'fresh', 'gentle', 'sparkling', 'precise', 'curious']\n", + " nouns = ['tiger', 'eagle', 'river', 'mountain', 'forest', 'ocean', 'star', 'moon', 'wind', 'dragon', 'phoenix', 'wolf', 'bear', 'lion', 'shark', 'falcon', 'raven', 'crystal', 'diamond', 'ruby', 'sapphire', 'pearl', 'wave', 'tide', 'cloud', 'rainbow', 'sunset', 'sunrise', 'galaxy', 'comet', 'meteor', 'planet', 'nebula', 'cosmos', 'universe', 'atom', 'photon', 'quantum', 'matrix', 'cipher', 'code', 'signal', 'pulse', 'beam', 'ray', 'spark', 'frost', 'ice', 'snow', 'mist', 'fog', 'dew', 'rain', 'hail', 'helix', 'prism', 'lens', 'mirror', 'echo', 'heart', 'mind', 'dream', 'vision', 'hope', 'wish', 'magic', 'spell', 'charm', 'rune', 'symbol', 'token', 'key', 'door', 'gate', 'bridge', 'tower', 'castle', 'fortress', 'shield', 'dolphin', 'whale', 'penguin', 'butterfly', 'hummingbird', 'deer', 'rabbit', 'fox', 'otter', 'panda', 'koala', 'zebra', 'giraffe', 'elephant', 'valley', 'canyon', 'meadow', 'prairie', 'island', 'lake', 'pond', 'stream', 'waterfall', 'cliff', 'peak', 'hill', 'grove', 'garden', 'sunlight', 'breeze', 'melody', 'sparkle', 'whirlpool', 'windmill', 'carousel', 'spiral', 'glow']\n", + " verbs = ['runs', 'flies', 'jumps', 'builds', 'creates', 'flows', 'shines', 'grows', 'moves', 'works', 'dances', 'sings', 'plays', 'dreams', 'thinks', 'learns', 'teaches', 'helps', 'heals', 'saves', 'protects', 'guards', 'watches', 'sees', 'hears', 'feels', 'knows', 'understands', 'discovers', 'explores', 'searches', 'finds', 'seeks', 'holds', 'carries', 'lifts', 'pushes', 'pulls', 'makes', 'crafts', 'forges', 'shapes', 'forms', 'molds', 'carves', 'joins', 'connects', 'links', 'binds', 'ties', 'opens', 'closes', 'starts', 'stops', 'begins', 'ends', 'finishes', 'completes', 'wins', 'triumphs', 'succeeds', 'achieves', 'accomplishes', 'reaches', 'arrives', 'departs', 'leaves', 'returns', 'comes', 'goes', 'travels', 'journeys', 'walks', 'sprints', 'races', 'speeds', 'rushes', 'hurries', 'waits', 'pauses', 'rests', 'sleeps', 'wakes', 'rises', 'climbs', 'ascends', 'descends', 'swims', 'dives', 'surfs', 'sails', 'paddles', 'hikes', 'treks', 'wanders', 'roams', 'ventures', 'navigates', 'glides', 'soars', 'floats', 'drifts', 'tosses', 'divides', 'shares', 'secures', 'settles', 'places', 'wonders', 'questions']\n", + " adverbs = ['quickly', 'gracefully', 'gently', 'boldly', 'quietly', 'swiftly', 'carefully', 'eagerly', 'smoothly', 'brightly', 'softly', 'steadily', 'cleverly', 'proudly', 'calmly', 'freely', 'wisely', 'kindly', 'firmly', 'lightly', 'deeply', 'clearly', 'warmly', 'coolly', 'sharply', 'slowly', 'rapidly', 'silently', 'loudly', 'perfectly']\n", + " suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=suffix))\n", + " words = [random.choice(o) for o in (adjectives,nouns,verbs,adverbs)[:levels]]\n", + " return f\"{'-'.join(words)}-{suffix}\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`friendly_name` generates random, human-readable names by combining adjectives, nouns, verbs, and adverbs with a random alphanumeric suffix. This is useful for creating memorable identifiers for temporary files, test data, or user-friendly resource names." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'cool-echo-flies-mwzs'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "friendly_name() # Default: 3 word levels + 4-char suffix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Names are hyphen-separated and follow the pattern `adjective-noun-verb-adverb`, randomly chosen from lists of size 102, 116, 110, and 30, respectively. The `levels` param selects how many of the names to include:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'pensive-signal-mywn'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "friendly_name(2) # 2 words + 4-char suffix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`suffix` sets the length of the random alphanumeric ending. Each suffix item is taken from the 36 options of lowercase letters plus digits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'flamboyant-gate-achieves-boldly-5kxd6w'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "friendly_name(4, 6) # All 4 word types + 6-char suffix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def n_friendly_names(levels=3, suffix=4):\n", + " \"Number of possible combos for `friendly_names\"\n", + " ns = [102,116,110,30]\n", + " return product(ns[:levels])*36**suffix" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The number of combinations if all levels are included is:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "65,581,614,489,600\n" + ] + } + ], + "source": [ + "print(f'{n_friendly_names(4):,}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The default settings give:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2,186,053,816,320\n" + ] + } + ], + "source": [ + "print(f'{n_friendly_names():,}')" + ] + }, { "cell_type": "code", "execution_count": null, From b14c2baad64cba1860fc1a7b50b845b40f19622a Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 29 Sep 2025 10:51:37 +1000 Subject: [PATCH 122/182] fixes #699 --- fastcore/_modidx.py | 3 +++ fastcore/xtras.py | 25 ++++++++++++++++++++++--- nbs/03_xtras.ipynb | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 61361590..e5ee2046 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -652,12 +652,15 @@ 'fastcore.xtras.repo_details': ('xtras.html#repo_details', 'fastcore/xtras.py'), 'fastcore.xtras.repr_dict': ('xtras.html#repr_dict', 'fastcore/xtras.py'), 'fastcore.xtras.round_multiple': ('xtras.html#round_multiple', 'fastcore/xtras.py'), + 'fastcore.xtras.rsync_multi': ('xtras.html#rsync_multi', 'fastcore/xtras.py'), 'fastcore.xtras.run': ('xtras.html#run', 'fastcore/xtras.py'), 'fastcore.xtras.save_iter': ('xtras.html#save_iter', 'fastcore/xtras.py'), 'fastcore.xtras.save_pickle': ('xtras.html#save_pickle', 'fastcore/xtras.py'), 'fastcore.xtras.set_num_threads': ('xtras.html#set_num_threads', 'fastcore/xtras.py'), + 'fastcore.xtras.shell': ('xtras.html#shell', 'fastcore/xtras.py'), 'fastcore.xtras.shufflish': ('xtras.html#shufflish', 'fastcore/xtras.py'), 'fastcore.xtras.sparkline': ('xtras.html#sparkline', 'fastcore/xtras.py'), + 'fastcore.xtras.ssh': ('xtras.html#ssh', 'fastcore/xtras.py'), 'fastcore.xtras.stringfmt_names': ('xtras.html#stringfmt_names', 'fastcore/xtras.py'), 'fastcore.xtras.time_policy': ('xtras.html#time_policy', 'fastcore/xtras.py'), 'fastcore.xtras.timed_cache': ('xtras.html#timed_cache', 'fastcore/xtras.py'), diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 2ee18483..18ee6f82 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -7,9 +7,9 @@ # %% auto 0 __all__ = ['spark_chars', 'UNSET', 'walk', 'globtastic', 'maybe_open', 'mkdir', 'image_size', 'bunzip', 'loads', 'loads_multi', - 'dumps', 'untar_dir', 'repo_details', 'run', 'open_file', 'save_pickle', 'load_pickle', 'parse_env', - 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'mapped', 'IterLen', - 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', 'friendly_name', + 'dumps', 'untar_dir', 'repo_details', 'shell', 'ssh', 'rsync_multi', 'run', 'open_file', 'save_pickle', + 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'mapped', + 'IterLen', 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', 'friendly_name', 'n_friendly_names', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', 'ContextManagers', @@ -198,6 +198,25 @@ def repo_details(url): res = res.split(':')[-1] return res.split('/')[-2:] +# %% ../nbs/03_xtras.ipynb +def shell(*args, **kwargs): + "Shortcut for `subprocess.run(shell=True)`" + import subprocess + return subprocess.run(*args, shell=True, **kwargs) + +# %% ../nbs/03_xtras.ipynb +def ssh(host, args='', user='ubuntu', sock=None): + "Run SSH command with given arguments" + sock_opts = f'-S {sock}' if sock else '' + return shell(f'ssh {sock_opts} {args} {user}@{host}') + +# %% ../nbs/03_xtras.ipynb +def rsync_multi(ip, files, user='ubuntu', persist='5m'): + "Transfer multiple files with rename using persistent SSH connection" + sock = f'/tmp/ssh-{ip}-{user}' + ssh(ip, f'-o ControlMaster=auto -o ControlPersist={persist} -N -f', user, sock) + for src,dst in files: shell(f'rsync -az -e "ssh -S {sock}" {src} {user}@{ip}:{dst}') + # %% ../nbs/03_xtras.ipynb def run(cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, stderr=False): "Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails" diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 8d23dd9c..607e3897 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -665,6 +665,46 @@ "test_eq(repo_details('git@github.com:fastai/nbdev.git\\n'), ['fastai', 'nbdev'])" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def shell(*args, **kwargs):\n", + " \"Shortcut for `subprocess.run(shell=True)`\"\n", + " import subprocess\n", + " return subprocess.run(*args, shell=True, **kwargs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def ssh(host, args='', user='ubuntu', sock=None):\n", + " \"Run SSH command with given arguments\"\n", + " sock_opts = f'-S {sock}' if sock else ''\n", + " return shell(f'ssh {sock_opts} {args} {user}@{host}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def rsync_multi(ip, files, user='ubuntu', persist='5m'):\n", + " \"Transfer multiple files with rename using persistent SSH connection\"\n", + " sock = f'/tmp/ssh-{ip}-{user}'\n", + " ssh(ip, f'-o ControlMaster=auto -o ControlPersist={persist} -N -f', user, sock)\n", + " for src,dst in files: shell(f'rsync -az -e \"ssh -S {sock}\" {src} {user}@{ip}:{dst}')" + ] + }, { "cell_type": "code", "execution_count": null, From c75b1b76c67e35f54eb19f4808b7adeacf43488d Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 29 Sep 2025 10:52:13 +1000 Subject: [PATCH 123/182] release --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69337298..2a7b50e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ +## 1.8.12 + +### New Features + +- Add `friendly_name` ([#698](https://github.com/AnswerDotAI/fastcore/issues/698)) +- Add `product` ([#698](https://github.com/AnswerDotAI/fastcore/issues/698)) +- Add `shell`, `ssh`, and `rsync_multi` ([#699](https://github.com/AnswerDotAI/fastcore/issues/699)) + + ## 1.8.11 ### New Features From f559c1b04acaa832b53ac0d7b95b6281a259d226 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 29 Sep 2025 10:56:21 +1000 Subject: [PATCH 124/182] fix tests --- nbs/03_xtras.ipynb | 46 +++++++++++++++++++++++----------------------- nbs/09_xml.ipynb | 23 ++++++++++++++++++++++- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 607e3897..65fc5a87 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -218,9 +218,9 @@ " with maybe_open(fn) as f: return f.encoding\n", "\n", "fname = '00_test.ipynb'\n", - "sys_encoding = 'cp1252' if sys.platform == 'win32' else 'UTF-8'\n", - "test_eq(_f(fname), sys_encoding)\n", - "with open(fname) as fh: test_eq(_f(fh), sys_encoding)" + "sys_encoding = 'cp1252' if sys.platform == 'win32' else 'utf-8'\n", + "test_eq(_f(fname).lower(), sys_encoding)\n", + "with open(fname) as fh: test_eq(_f(fh).lower(), sys_encoding)" ] }, { @@ -1504,7 +1504,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L394){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L413){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1515,7 +1515,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L394){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L413){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1587,7 +1587,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L405){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L424){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1598,7 +1598,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L405){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L424){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1687,7 +1687,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L409){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L428){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1698,7 +1698,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L409){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L428){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1752,7 +1752,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L425){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -1763,7 +1763,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L406){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L425){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -1796,7 +1796,7 @@ { "data": { "text/plain": [ - "['d', 'c', 'a', 'b', 'e', 'h', 'g', 'f']" + "['c', 'b', 'e', 'a', 'd', 'h', 'g', 'f']" ] }, "execution_count": null, @@ -2168,7 +2168,7 @@ { "data": { "text/plain": [ - "'cool-echo-flies-mwzs'" + "'tender-otter-sprints-p753'" ] }, "execution_count": null, @@ -2195,7 +2195,7 @@ { "data": { "text/plain": [ - "'pensive-signal-mywn'" + "'kind-sunrise-9bib'" ] }, "execution_count": null, @@ -2222,7 +2222,7 @@ { "data": { "text/plain": [ - "'flamboyant-gate-achieves-boldly-5kxd6w'" + "'sweet-signal-goes-loudly-wk347d'" ] }, "execution_count": null, @@ -2801,7 +2801,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L573){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L613){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### EventTimer\n", "\n", @@ -2812,7 +2812,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L573){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L613){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### EventTimer\n", "\n", @@ -2846,8 +2846,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Num Events: 1, Freq/sec: 161.2\n", - "Most recent: ▁▇▆▁▃ 59.3 132.1 120.0 58.9 92.0\n" + "Num Events: 2, Freq/sec: 74.1\n", + "Most recent: ▁▁▂▇▅ 24.6 24.5 47.3 90.9 65.5\n" ] } ], @@ -2926,7 +2926,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L605){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L645){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### PartialFormatter\n", "\n", @@ -2937,7 +2937,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L605){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L645){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### PartialFormatter\n", "\n", @@ -3142,7 +3142,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L662){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L702){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ContextManagers\n", "\n", @@ -3153,7 +3153,7 @@ "text/plain": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L662){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L702){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ContextManagers\n", "\n", diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb index ca652e4a..be281f66 100644 --- a/nbs/09_xml.ipynb +++ b/nbs/09_xml.ipynb @@ -508,12 +508,33 @@ "test_eq(to_xml(b\"Bytes\"), 'Bytes')\n", "test_eq(to_xml(Div(P(\"Text\"), B(\"Bold\")), indent=False), '

Text

Bold
')\n", "test_eq(to_xml(Div(\"\"), do_escape=True),\n", - " '
<script>alert('XSS')</script>
\\n')\n", + " \"
<script>alert('XSS')</script>
\\n\")\n", "test_eq(to_xml(Div(\"\"), do_escape=False),\n", " \"
\\n\")\n", "test_eq(to_xml(Div(foo=False), indent=False), '
')" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "204c7d72", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"
<script>alert('XSS')</script>
\\n\"" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "to_xml(Div(\"\"), do_escape=True)" + ] + }, { "cell_type": "code", "execution_count": null, From 158012525dd174d07d5c2833233c0c2f93c1fe39 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 29 Sep 2025 10:56:36 +1000 Subject: [PATCH 125/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 0a400907..880e5798 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.12" +__version__ = "1.8.13" diff --git a/settings.ini b/settings.ini index a695f1b7..8209e12f 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.12 +version = 1.8.13 min_python = 3.10 audience = Developers language = English From ed34f4b0ef9827402191c103ad68d2302cb12c15 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 29 Sep 2025 11:01:08 +1000 Subject: [PATCH 126/182] fix test --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d687640..ac020a84 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,7 +51,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: '3.9' + python-version: '3.12' - name: clone this branch [fastcore] uses: actions/checkout@v3 with: From 1b80db61161d619f9f7a579d01cfb9e937c9b619 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 17 Oct 2025 12:33:00 +1000 Subject: [PATCH 127/182] fixes #701 --- fastcore/basics.py | 13 + nbs/01_basics.ipynb | 582 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 580 insertions(+), 15 deletions(-) diff --git a/fastcore/basics.py b/fastcore/basics.py index 6c03e859..f569b83f 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -255,6 +255,19 @@ def chunked(it, chunk_sz=None, drop_last=False, n_chunks=None): if res and (len(res)==chunk_sz or not drop_last): yield res if len(res) Date: Fri, 17 Oct 2025 12:43:00 +1000 Subject: [PATCH 128/182] fixes #700 --- fastcore/basics.py | 6 +- nbs/01_basics.ipynb | 347 +++++++++++++++++++++----------------------- 2 files changed, 170 insertions(+), 183 deletions(-) diff --git a/fastcore/basics.py b/fastcore/basics.py index f569b83f..ffde55bf 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -360,13 +360,17 @@ def eval_type(t, glb, loc): return t # %% ../nbs/01_basics.ipynb +_allowed_types = (types.FunctionType, types.BuiltinFunctionType, types.MethodType, + types.ModuleType, types.WrapperDescriptorType, types.MethodWrapperType, + types.MethodDescriptorType) + def _eval_type(t, glb, loc): res = eval_type(t, glb, loc) return NoneType if res is None else res def type_hints(f): "Like `typing.get_type_hints` but returns `{}` if not allowed type" - if not isinstance(f, typing._allowed_types): return {} + if not isinstance(f, _allowed_types): return {} ann,glb,loc = get_annotations_ex(f) return {k:_eval_type(v,glb,loc) for k,v in ann.items()} diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 6c693ac3..c1742eac 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -17,7 +17,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from fastcore.imports import *\n", "import ast,builtins,pprint,types,typing\n", "from functools import cmp_to_key\n", @@ -34,7 +34,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from __future__ import annotations\n", "from fastcore.test import *\n", "from nbdev.showdoc import *\n", @@ -66,7 +66,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "defaults = SimpleNamespace()" ] }, @@ -77,7 +77,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def ifnone(a, b):\n", " \"`b` if `a` is None else `a`\"\n", " return b if a is None else a" @@ -109,7 +109,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def maybe_attr(o, attr):\n", " \"`getattr(o,attr,o)`\"\n", " return getattr(o,attr,o)" @@ -143,7 +143,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def basic_repr(flds=None):\n", " \"Minimal `__repr__`\"\n", " if isinstance(flds, str): flds = re.split(', *', flds)\n", @@ -297,7 +297,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class BasicRepr:\n", " \"Base class for objects needing a basic `__repr__`\"\n", " __repr__=basic_repr()" @@ -342,7 +342,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def is_array(x):\n", " \"`True` if `x` supports `__array__` or `iloc`\"\n", " return hasattr(x,'__array__') or hasattr(x,'iloc')" @@ -376,7 +376,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def listify(o=None, *rest, use_list=False, match=None):\n", " \"Convert `o` to a `list`\"\n", " if rest: o = (o,)+rest\n", @@ -544,7 +544,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def tuplify(o, use_list=False, match=None):\n", " \"Make `o` a tuple\"\n", " return tuple(listify(o, use_list=use_list, match=match))" @@ -569,7 +569,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def true(x):\n", " \"Test whether `x` is truthy; collections with >0 elements are considered `True`\"\n", " try: return bool(len(x))\n", @@ -612,7 +612,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class NullType:\n", " \"An object that is `False` and can be called, chained, and indexed\"\n", " def __getattr__(self,*args):return null\n", @@ -651,7 +651,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def tonull(x):\n", " \"Convert `None` to `null`\"\n", " return null if x is None else x" @@ -685,7 +685,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def get_class(nm, *fld_names, sup=None, doc=None, funcs=None, anno=None, **flds):\n", " \"Dynamically create a class, optionally inheriting from `sup`, containing `fld_names`\"\n", " attrs = {}\n", @@ -805,7 +805,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def mk_class(nm, *fld_names, sup=None, doc=None, funcs=None, mod=None, anno=None, **flds):\n", " \"Create a class using `get_class` and add to the caller's module\"\n", " if mod is None: mod = sys._getframe(1).f_locals\n", @@ -878,7 +878,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def wrap_class(nm, *fld_names, sup=None, doc=None, funcs=None, **flds):\n", " \"Decorator: makes function a method of a new class `nm` passing parameters to `mk_class`\"\n", " def _inner(f):\n", @@ -909,7 +909,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class ignore_exceptions:\n", " \"Context manager to ignore exceptions\"\n", " def __enter__(self): pass\n", @@ -975,7 +975,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def exec_local(code, var_name):\n", " \"Call `exec` on `code` and return the var `var_name`\"\n", " loc = {}\n", @@ -1000,7 +1000,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _risinstance(types, obj):\n", " if any(isinstance(t,str) for t in types):\n", " return any(t.__name__ in types for t in type(obj).__mro__)\n", @@ -1204,8 +1204,8 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", - "#|hide\n", + "#| export\n", + "#| hide\n", "class _InfMeta(type):\n", " @property\n", " def count(self): return itertools.count()\n", @@ -1224,7 +1224,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class Inf(metaclass=_InfMeta):\n", " \"Infinite lists\"\n", " pass" @@ -1288,7 +1288,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "_dumobj = object()\n", "def _oper(op,a,b=_dumobj): return (lambda o:op(o,a)) if b is _dumobj else op(a,b)\n", "\n", @@ -1308,7 +1308,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def in_(x, a):\n", " \"`True` if `x in a`\"\n", " return x in a\n", @@ -1323,7 +1323,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "_all_ = ['lt','gt','le','ge','eq','ne','add','sub','mul','truediv','is_','is_not','in_', 'mod']" ] }, @@ -1334,7 +1334,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "for op in _all_: _mk_op(op, globals())" ] }, @@ -1422,7 +1422,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def ret_true(*args, **kwargs):\n", " \"Predicate: always `True`\"\n", " return True" @@ -1446,7 +1446,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def ret_false(*args, **kwargs):\n", " \"Predicate: always `False`\"\n", " return False" @@ -1459,7 +1459,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def stop(e=StopIteration):\n", " \"Raises exception `e` (by default `StopIteration`)\"\n", " raise e" @@ -1472,7 +1472,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def gen(func, seq, cond=ret_true):\n", " \"Like `(func(o) for o in seq if cond(func(o)))` but handles `StopIteration`\"\n", " return itertools.takewhile(cond, map(func,seq))" @@ -1500,7 +1500,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def chunked(it, chunk_sz=None, drop_last=False, n_chunks=None):\n", " \"Return batches from iterator `it` of size `chunk_sz` (or return `n_chunks` total)\"\n", " assert bool(chunk_sz) ^ bool(n_chunks)\n", @@ -1519,7 +1519,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def chunked(it, chunk_sz=None, drop_last=False, n_chunks=None, pad=False, pad_val=None):\n", " \"Return batches from iterator `it` of size `chunk_sz` (or return `n_chunks` total)\"\n", " assert bool(chunk_sz) ^ bool(n_chunks)\n", @@ -1594,7 +1594,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def otherwise(x, tst, y):\n", " \"`y if tst(x) else x`\"\n", " return y if tst(x) else x" @@ -1634,7 +1634,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def custom_dir(c, add):\n", " \"Implement custom `__dir__`, adding `add` to `cls`\"\n", " return object.__dir__(c) + listify(add)" @@ -1669,7 +1669,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class AttrDict(dict):\n", " \"`dict` subclass that also provides access to keys as attrs\"\n", " def __getattr__(self,k): return self[k] if k in self else stop(AttributeError(k))\n", @@ -1752,7 +1752,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class AttrDictDefault(AttrDict):\n", " \"`AttrDict` subclass that returns `None` for missing attrs\"\n", " def __init__(self, *args, default_=None, **kwargs):\n", @@ -1782,7 +1782,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class NS(SimpleNamespace):\n", " \"`SimpleNamespace` subclass that also adds `iter` and `dict` support\"\n", " def __iter__(self): return iter(self.__dict__)\n", @@ -1890,7 +1890,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def get_annotations_ex(obj, *, globals=None, locals=None):\n", " \"Backport of py3.10 `get_annotations` that returns globals/locals\"\n", " if isinstance(obj, type):\n", @@ -1953,7 +1953,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def eval_type(t, glb, loc):\n", " \"`eval` a type or collection of types, if needed, for annotations in py3.10+\"\n", " if isinstance(t,str):\n", @@ -2032,55 +2032,38 @@ { "cell_type": "code", "execution_count": null, - "id": "ba1104e4", + "id": "7e8c9e8a", "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", + "_allowed_types = (types.FunctionType, types.BuiltinFunctionType, types.MethodType, \n", + " types.ModuleType, types.WrapperDescriptorType, types.MethodWrapperType,\n", + " types.MethodDescriptorType)\n", + "\n", "def _eval_type(t, glb, loc):\n", " res = eval_type(t, glb, loc)\n", " return NoneType if res is None else res\n", "\n", "def type_hints(f):\n", " \"Like `typing.get_type_hints` but returns `{}` if not allowed type\"\n", - " if not isinstance(f, typing._allowed_types): return {}\n", + " if not isinstance(f, _allowed_types): return {}\n", " ann,glb,loc = get_annotations_ex(f)\n", " return {k:_eval_type(v,glb,loc) for k,v in ann.items()}" ] }, - { - "cell_type": "markdown", - "id": "eb1ebdb9", - "metadata": {}, - "source": [ - "Below is a list of allowed types for type hints in python:" - ] - }, { "cell_type": "code", "execution_count": null, - "id": "8bf201ee", + "id": "6d61b241", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[function,\n", - " builtin_function_or_method,\n", - " method,\n", - " module,\n", - " wrapper_descriptor,\n", - " method-wrapper,\n", - " method_descriptor]" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "list(typing._allowed_types)" + "def type_hints(f):\n", + " \"Like `typing.get_type_hints` but returns `{}` if not allowed type\"\n", + " if not isinstance(f, _allowed_types): return {}\n", + " ann,glb,loc = get_annotations_ex(f)\n", + " return {k:_eval_type(v,glb,loc) for k,v in ann.items()}" ] }, { @@ -2131,7 +2114,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def annotations(o):\n", " \"Annotations for `o`, or `type(o)`\"\n", " res = {}\n", @@ -2169,7 +2152,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def anno_ret(func):\n", " \"Get the return annotation of `func`\"\n", " return annotations(func).get('return', None) if func else None" @@ -2238,7 +2221,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _ispy3_10(): return sys.version_info.major >=3 and sys.version_info.minor >=10\n", "\n", "def signature_ex(obj, eval_str:bool=False):\n", @@ -2265,7 +2248,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def union2tuple(t):\n", " if (getattr(t, '__origin__', None) is Union\n", " or (UnionType and isinstance(t, UnionType))): return t.__args__\n", @@ -2293,7 +2276,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def argnames(f, frame=False):\n", " \"Names of arguments to function or frame `f`\"\n", " code = getattr(f, 'f_code' if frame else '__code__')\n", @@ -2317,7 +2300,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def with_cast(f):\n", " \"Decorator which uses any parameter annotations as preprocessing functions\"\n", " anno, out_anno, params = annotations(f), anno_ret(f), argnames(f)\n", @@ -2364,7 +2347,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _store_attr(self, anno, **attrs):\n", " stored = getattr(self, '__stored_args__', None)\n", " for n,v in attrs.items():\n", @@ -2380,7 +2363,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def store_attr(names=None, self=None, but='', cast=False, store_args=None, **attrs):\n", " \"Store params named in comma-separated `names` from calling context into attrs in `self`\"\n", " fr = sys._getframe(1)\n", @@ -2514,7 +2497,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "class _T:\n", " def __init__(self, a,b):\n", " c = 2\n", @@ -2632,7 +2615,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "# ensure that subclasses work with or without `store_attr`\n", "class T4(T):\n", " def __init__(self, **kwargs):\n", @@ -2655,7 +2638,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "#ensure that kwargs work with names==None\n", "class T:\n", " def __init__(self, a,b,c,**kwargs): store_attr(**kwargs)\n", @@ -2671,7 +2654,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "#ensure that kwargs work with names==''\n", "class T:\n", " def __init__(self, a, **kwargs):\n", @@ -2759,7 +2742,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def attrdict(o, *ks, default=None):\n", " \"Dict from each `k` in `ks` to `getattr(o,k)`\"\n", " return {k:getattr(o, k, default) for k in ks}" @@ -2786,7 +2769,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def properties(cls, *ps):\n", " \"Change attrs in `cls` with names in `ps` to properties\"\n", " for p in ps: setattr(cls,p,property(getattr(cls,p)))" @@ -2815,7 +2798,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "_c2w_re = re.compile(r'((?<=[a-z])[A-Z]|(?list:\n", " \"Concatenate all collections and items as a list\"\n", " return list(flatten(colls))" @@ -3786,7 +3769,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def strcat(its, sep:str='')->str:\n", " \"Concatenate stringified items `its`\"\n", " return sep.join(map(str,its))" @@ -3810,7 +3793,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def detuplify(x):\n", " \"If `x` is a tuple with one thing, extract it\"\n", " return None if len(x)==0 else x[0] if len(x)==1 and getattr(x, 'ndim', 1)==1 else x" @@ -3836,7 +3819,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def replicate(item,match):\n", " \"Create tuple of `item` copied `len(match)` times\"\n", " return (item,)*len(match)" @@ -3861,7 +3844,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def setify(o):\n", " \"Turn any list like-object into a set.\"\n", " return o if isinstance(o,set) else set(listify(o))" @@ -3889,7 +3872,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def merge(*ds):\n", " \"Merge all dictionaries in `ds`\"\n", " return {k:v for d in ds if d is not None for k,v in d.items()}" @@ -3914,7 +3897,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def range_of(x):\n", " \"All indices of collection `x` (i.e. `list(range(len(x)))`)\"\n", " return list(range(len(x)))" @@ -3937,7 +3920,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _conv_key(k):\n", " if isinstance(k,int): return itemgetter(k)\n", " elif isinstance(k,str): return attrgetter(k)\n", @@ -4036,7 +4019,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def last_index(x, o):\n", " \"Finds the last index of occurence of `x` in `o` (returns -1 if no occurence)\"\n", " try: return next(i for i in reversed(range(len(o))) if o[i] == x)\n", @@ -4061,7 +4044,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def filter_dict(d, func):\n", " \"Filter a `dict` using `func`, applied to keys and values\"\n", " return {k:v for k,v in d.items() if func(k,v)}" @@ -4117,7 +4100,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def filter_keys(d, func):\n", " \"Filter a `dict` using `func`, applied to keys\"\n", " return {k:v for k,v in d.items() if func(k)}" @@ -4151,7 +4134,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def filter_values(d, func):\n", " \"Filter a `dict` using `func`, applied to values\"\n", " return {k:v for k,v in d.items() if func(v)}" @@ -4185,7 +4168,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def cycle(o):\n", " \"Like `itertools.cycle` except creates list of `None`s if `o` is empty\"\n", " o = listify(o)\n", @@ -4212,7 +4195,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def zip_cycle(x, *args):\n", " \"Like `itertools.zip_longest` but `cycle`s through elements of all but first argument\"\n", " return zip(x, *map(cycle,args))" @@ -4235,7 +4218,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def sorted_ex(iterable, key=None, reverse=False, cmp=None, **kwargs):\n", " \"Like `sorted`, but if key is str use `attrgetter`; if int use `itemgetter`; use `cmp` comparator function or `key` with `kwargs`\"\n", " if callable(key) and kwargs: k=partial(key, **kwargs)\n", @@ -4368,7 +4351,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def not_(f):\n", " \"Create new function that negates result of `f`\"\n", " def _f(*args, **kwargs): return not f(*args, **kwargs)\n", @@ -4395,7 +4378,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def argwhere(iterable, f, negate=False, **kwargs):\n", " \"Like `filter_ex`, but return indices for matching items\"\n", " if kwargs: f = partial(f,**kwargs)\n", @@ -4410,7 +4393,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def filter_ex(iterable, f=noop, negate=False, gen=False, **kwargs):\n", " \"Like `filter`, but passing `kwargs` to `f`, defaulting `f` to `noop`, and adding `negate` and `gen`\"\n", " if f is None: f = lambda _: True\n", @@ -4428,7 +4411,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def range_of(a, b=None, step=None):\n", " \"All indices of collection `a`, if `a` is a collection, otherwise `range`\"\n", " if is_coll(a): a = len(a)\n", @@ -4453,7 +4436,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def renumerate(iterable, start=0):\n", " \"Same as `enumerate`, but returns index as 2nd element instead of 1st\"\n", " return ((o,i) for i,o in enumerate(iterable, start=start))" @@ -4476,7 +4459,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def first(x, f=None, negate=False, **kwargs):\n", " \"First element of `x`, optionally filtered by `f`, or None if missing\"\n", " x = iter(x)\n", @@ -4503,7 +4486,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def last(x, f=None, negate=False, **kwargs):\n", " \"Last element of `x`, optionally filtered by `f`, or None if missing\"\n", " if f: x = filter_ex(x, f=f, negate=negate, gen=True, **kwargs)\n", @@ -4531,7 +4514,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def only(o):\n", " \"Return the only item of `o`, raise if `o` doesn't have exactly one item\"\n", " it = iter(o)\n", @@ -4549,7 +4532,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "test_fail(lambda: only([]), contains='iterable has 0 items')\n", "test_eq(only([0]), 0)\n", "test_fail(lambda: only([0,1]), contains='iterable has more than 1 item')" @@ -4562,7 +4545,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def nested_attr(o, attr, default=None):\n", " \"Same as `getattr`, but if `attr` includes a `.`, then looks inside nested objects\"\n", " try:\n", @@ -4636,7 +4619,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "class CustomIndexable:\n", " def __init__(self): self.data = {'a':1,'b':'v','c':{'d':5}}\n", " def __getitem__(self, key): return self.data[key]\n", @@ -4654,7 +4637,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def nested_setdefault(o, attr, default):\n", " \"Same as `setdefault`, but if `attr` includes a `.`, then looks inside nested objects\"\n", " attrs = attr.split('.')\n", @@ -4669,7 +4652,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "o = {'e':'f'}\n", "test_eq(nested_setdefault(o, 'a.b.c', 'd'), 'd')\n", "test_eq(o, {'a':{'b':{'c':'d'}},'e':'f'})" @@ -4682,7 +4665,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "o = {'a':{'b':'c'}}\n", "test_eq(nested_setdefault(o, 'a.b', 'd'), 'c')\n", "test_eq(o,{'a':{'b':'c'}})" @@ -4695,7 +4678,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def nested_callable(o, attr):\n", " \"Same as `nested_attr` but if not found will return `noop`\"\n", " return nested_attr(o, attr, noop)" @@ -4720,7 +4703,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _access(coll, idx):\n", " if isinstance(idx,str) and hasattr(coll, idx): return getattr(coll, idx)\n", " if hasattr(coll, 'get'): return coll.get(idx, None)\n", @@ -4746,7 +4729,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def nested_idx(coll, *idxs):\n", " \"Index into nested collections, dicts, etc, with `idxs`\"\n", " if not coll or not idxs: return coll\n", @@ -4796,7 +4779,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def set_nested_idx(coll, value, *idxs):\n", " \"Set value indexed like `nested_idx\"\n", " coll,idx = _nested_idx(coll, *idxs)\n", @@ -4821,7 +4804,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def val2idx(x):\n", " \"Dict from value to index\"\n", " return {v:k for k,v in enumerate(x)}" @@ -4844,7 +4827,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def uniqueify(x, sort=False, bidir=False, start=None):\n", " \"Unique elements in `x`, optional `sort`, optional return reverse correspondence, optional prepend with elements.\"\n", " res = list(dict.fromkeys(x))\n", @@ -4879,7 +4862,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "\n", "# looping functions from https://github.com/willmcgugan/rich/blob/master/rich/_loop.py\n", "def loop_first_last(values):\n", @@ -4911,7 +4894,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def loop_first(values):\n", " \"Iterate and generate a tuple with a flag for first value.\"\n", " return ((b,o) for b,_,o in loop_first_last(values))" @@ -4934,7 +4917,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def loop_last(values):\n", " \"Iterate and generate a tuple with a flag for last value.\"\n", " return ((b,o) for _,b,o in loop_first_last(values))" @@ -5020,7 +5003,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "num_methods = \"\"\"\n", " __add__ __sub__ __mul__ __matmul__ __truediv__ __floordiv__ __mod__ __divmod__ __pow__\n", " __lshift__ __rshift__ __and__ __xor__ __or__ __neg__ __pos__ __abs__\n", @@ -5042,7 +5025,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class fastuple(tuple):\n", " \"A `tuple` with elementwise ops and more friendly __init__ behavior\"\n", " def __new__(cls, x=None, *rest):\n", @@ -5362,7 +5345,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class _Arg:\n", " def __init__(self,i): self.i = i\n", "arg0 = _Arg(0)\n", @@ -5379,7 +5362,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class bind:\n", " \"Same as `partial`, except you can use `arg0` `arg1` etc param placeholders\"\n", " def __init__(self, func, *pargs, **pkwargs):\n", @@ -5543,7 +5526,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def mapt(func, *iterables):\n", " \"Tuplified `map`\"\n", " return tuple(map(func, *iterables))" @@ -5567,7 +5550,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def map_ex(iterable, f, *args, gen=False, **kwargs):\n", " \"Like `map`, but use `bind`, and supports `str` and indexing\"\n", " g = (bind(f,*args,**kwargs) if callable(f)\n", @@ -5650,7 +5633,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def compose(*funcs, order=None):\n", " \"Create a function that composes all functions in `funcs`, passing along remaining `*args` and `**kwargs` to all\"\n", " funcs = listify(funcs)\n", @@ -5687,7 +5670,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def maps(*args, retain=noop):\n", " \"Like `map`, except funcs are composed first\"\n", " f = compose(*args[:-1])\n", @@ -5714,7 +5697,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def partialler(f, *args, order=None, **kwargs):\n", " \"Like `functools.partial` but also copies over docstring\"\n", " fnew = partial(f,*args,**kwargs)\n", @@ -5781,7 +5764,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def instantiate(t):\n", " \"Instantiate `t` if it's a type, otherwise do nothing\"\n", " return t() if isinstance(t, type) else t" @@ -5805,7 +5788,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _using_attr(f, attr, x): return f(getattr(x,attr))" ] }, @@ -5816,7 +5799,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def using_attr(f, attr):\n", " \"Construct a function which applies `f` to the argument's attribute `attr`\"\n", " return partial(_using_attr, f, attr)" @@ -5857,7 +5840,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class _Self:\n", " \"An alternative to `lambda` for calling methods on passed object.\"\n", " def __init__(self): self.nms,self.args,self.kwargs,self.ready = [],[],[],True\n", @@ -5897,7 +5880,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class _SelfCls:\n", " def __getattr__(self,k): return getattr(_Self(),k)\n", " def __getitem__(self,i): return self.__getattr__('__getitem__')(i)\n", @@ -5913,7 +5896,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "_all_ = ['Self']" ] }, @@ -6009,7 +5992,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def copy_func(f):\n", " \"Copy a non-builtin function (NB `copy.copy` does not work for this)\"\n", " if not isinstance(f,FunctionType): return copy(f)\n", @@ -6093,7 +6076,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class _clsmethod:\n", " def __init__(self, f): self.f = f\n", " def __get__(self, _, f_cls): return MethodType(self.f, f_cls)" @@ -6106,7 +6089,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def patch_to(cls, as_prop=False, cls_method=False, set_prop=False):\n", " \"Decorator: add `f` to `cls`\"\n", " if not isinstance(cls, (tuple,list)): cls=(cls,)\n", @@ -6289,7 +6272,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def patch(f=None, *, as_prop=False, cls_method=False, set_prop=False):\n", " \"Decorator: add `f` to the first parameter's class (based on f's type annotations)\"\n", " if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop)\n", @@ -6419,7 +6402,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def patch_property(f):\n", " \"Deprecated; use `patch(as_prop=True)` instead\"\n", " warnings.warn(\"`patch_property` is deprecated and will be removed; use `patch(as_prop=True)` instead\")\n", @@ -6471,7 +6454,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def compile_re(pat):\n", " \"Compile `pat` if it's not None\"\n", " return None if pat is None else re.compile(pat)" @@ -6495,7 +6478,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class ImportEnum(enum.Enum):\n", " \"An `Enum` that can have its values imported\"\n", " @classmethod\n", @@ -6566,7 +6549,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class StrEnum(str,ImportEnum):\n", " \"An `ImportEnum` that behaves like a `str`\"\n", " def __str__(self): return self.name" @@ -6621,7 +6604,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def str_enum(name, *vals):\n", " \"Simplified creation of `StrEnum` types\"\n", " return StrEnum(name, {o:o for o in vals})" @@ -6711,7 +6694,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class Stateful:\n", " \"A base class/mixin for objects that should not serialize all their state\"\n", " _stateattrs=()\n", @@ -6896,7 +6879,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class PrettyString(str):\n", " \"Little hack to get strings to show properly in Jupyter.\"\n", " def __repr__(self): return self" @@ -7011,7 +6994,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def even_mults(start, stop, n):\n", " \"Build log-stepped array from `start` to `stop` in `n` steps.\"\n", " if n==1: return stop\n", @@ -7039,7 +7022,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def num_cpus():\n", " \"Get number of cpus\"\n", " try: return len(os.sched_getaffinity(0))\n", @@ -7076,7 +7059,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def add_props(f, g=None, n=2):\n", " \"Create properties passing each of `range(n)` to f\"\n", " if g is None: return (property(partial(f,i)) for i in range(n))\n", @@ -7125,7 +7108,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _typeerr(arg, val, typ): return TypeError(f\"{arg}=={val} not {typ}\")" ] }, @@ -7136,7 +7119,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def str2bool(s):\n", " \"Case-insensitive convert string `s` too a bool (`y`,`yes`,`t`,`true`,`on`,`1`->`True`)\"\n", " if not isinstance(s,str): return bool(s)\n", @@ -7306,7 +7289,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def to_bool(arg): return str2bool(arg) if isinstance(arg, str) else bool(arg)\n", "def to_int(arg): return str2int(arg) if isinstance(arg, str) else int(arg)\n", "def to_float(arg): return str2float(arg) if isinstance(arg, str) else float(arg) \n", @@ -7325,7 +7308,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def typed(_func=None, *, cast=False):\n", " \"Decorator to check param and return types at runtime, with optional casting\"\n", " def decorator(f):\n", @@ -7558,7 +7541,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def exec_new(code):\n", " \"Execute `code` in a new environment and return it\"\n", " pkg = None if __name__=='__main__' else Path().cwd().name\n", @@ -7585,7 +7568,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def exec_import(mod, sym):\n", " \"Import `sym` from `mod` in a new environment\"\n", "# pref = '' if __name__=='__main__' or mod[0]=='.' else '.'\n", @@ -7824,7 +7807,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] }, From 5cc17e69d664e10588576cb118dbd11e2b31d4b7 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 17 Oct 2025 12:51:43 +1000 Subject: [PATCH 129/182] fixes #682 --- fastcore/_modidx.py | 2 ++ fastcore/xml.py | 9 ++++++++ nbs/09_xml.ipynb | 51 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 54 insertions(+), 8 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index e5ee2046..9f3fb4cc 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -514,8 +514,10 @@ 'fastcore.xdg.xdg_state_home': ('xdg.html#xdg_state_home', 'fastcore/xdg.py')}, 'fastcore.xml': { 'fastcore.xml.FT': ('xml.html#ft', 'fastcore/xml.py'), 'fastcore.xml.FT.__call__': ('xml.html#ft.__call__', 'fastcore/xml.py'), + 'fastcore.xml.FT.__eq__': ('xml.html#ft.__eq__', 'fastcore/xml.py'), 'fastcore.xml.FT.__getattr__': ('xml.html#ft.__getattr__', 'fastcore/xml.py'), 'fastcore.xml.FT.__getitem__': ('xml.html#ft.__getitem__', 'fastcore/xml.py'), + 'fastcore.xml.FT.__hash__': ('xml.html#ft.__hash__', 'fastcore/xml.py'), 'fastcore.xml.FT.__html__': ('xml.html#ft.__html__', 'fastcore/xml.py'), 'fastcore.xml.FT.__init__': ('xml.html#ft.__init__', 'fastcore/xml.py'), 'fastcore.xml.FT.__iter__': ('xml.html#ft.__iter__', 'fastcore/xml.py'), diff --git a/fastcore/xml.py b/fastcore/xml.py index d4f913cb..b1e3946f 100644 --- a/fastcore/xml.py +++ b/fastcore/xml.py @@ -220,6 +220,15 @@ def to_xml(elm, lvl=0, indent=True, do_escape=True): def __html__(self:FT): return to_xml(self, indent=False) FT.__str__ = FT.__html__ +# %% ../nbs/09_xml.ipynb +@patch +def __eq__(self:FT, other): + if not isinstance(other, FT): return False + return self.tag==other.tag and self.attrs==other.attrs and self.children==other.children + +@patch +def __hash__(self:FT): return hash((self.tag, tuple(sorted(self.attrs.items())), self.children)) + # %% ../nbs/09_xml.ipynb def highlight(s, lang='html'): "Markdown to syntax-highlight `s` in language `lang`" diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb index be281f66..854d048c 100644 --- a/nbs/09_xml.ipynb +++ b/nbs/09_xml.ipynb @@ -48,7 +48,7 @@ "from IPython.display import Markdown\n", "from pprint import pprint\n", "\n", - "from fastcore.test import test_eq" + "from fastcore.test import test_eq, test_ne" ] }, { @@ -743,6 +743,47 @@ "print(Div('ho'))" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd8a6aff", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def __eq__(self:FT, other):\n", + " if not isinstance(other, FT): return False\n", + " return self.tag==other.tag and self.attrs==other.attrs and self.children==other.children\n", + "\n", + "@patch\n", + "def __hash__(self:FT): return hash((self.tag, tuple(sorted(self.attrs.items())), self.children))" + ] + }, + { + "cell_type": "markdown", + "id": "1abc3f67", + "metadata": {}, + "source": [ + "`FT` object equality and hashing is based on tag, attrs, and children." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f665b3d", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(Div('hello', id='x'), Div('hello', id='x'))\n", + "test_ne(Div('hello'), Div('goodbye'))\n", + "test_ne(Div('hello', id='a'), Div('hello', id='b'))\n", + "test_ne(P('hello'), Div('hello'))\n", + "\n", + "test_eq(hash(Div('hello', id='x')), hash(Div('hello', id='x')))\n", + "assert hash(Div('hello')), hash(Div('goodbye'))" + ] + }, { "cell_type": "markdown", "id": "5ad30d7c", @@ -869,13 +910,7 @@ "source": [] } ], - "metadata": { - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } From 04cce0f6a4f90a07137449c35d9a1d1529e1f9c1 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 17 Oct 2025 12:53:13 +1000 Subject: [PATCH 130/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a7b50e4..8de9f577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.8.13 + +### New Features + +- Add pad to `chunked` ([#701](https://github.com/AnswerDotAI/fastcore/issues/701)) +- Implement `__eq__` and `__hash__` for fastcore.xml.FT for value-based comparison + + ## 1.8.12 ### New Features From d9ddd2ba0847b78d4a05e2e4a71099a4b09a9a42 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 17 Oct 2025 12:53:26 +1000 Subject: [PATCH 131/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 880e5798..d6578c97 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.13" +__version__ = "1.8.14" diff --git a/settings.ini b/settings.ini index 8209e12f..56034486 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.13 +version = 1.8.14 min_python = 3.10 audience = Developers language = English From a7fef51f95af7ce5c2ad3d2d45f2664396ccf52d Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 19 Oct 2025 07:49:07 +1000 Subject: [PATCH 132/182] fmt --- nbs/000_tour.ipynb | 59 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/nbs/000_tour.ipynb b/nbs/000_tour.ipynb index 0d162645..cc2b43d3 100644 --- a/nbs/000_tour.ipynb +++ b/nbs/000_tour.ipynb @@ -3,6 +3,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4636a39a", "metadata": {}, "outputs": [], "source": [ @@ -14,6 +15,7 @@ }, { "cell_type": "markdown", + "id": "d99e49d2", "metadata": {}, "source": [ "# A tour of fastcore" @@ -21,6 +23,7 @@ }, { "cell_type": "markdown", + "id": "8a5d405f", "metadata": {}, "source": [ "Here's a (somewhat) quick tour of a few higlights from fastcore." @@ -28,6 +31,7 @@ }, { "cell_type": "markdown", + "id": "50c03be8", "metadata": {}, "source": [ "### Documentation" @@ -35,6 +39,7 @@ }, { "cell_type": "markdown", + "id": "7a5091fd", "metadata": {}, "source": [ "All fast.ai projects, including this one, are built with [nbdev](https://nbdev.fast.ai), which is a full literate programming environment built on Jupyter Notebooks. That means that every piece of documentation, including the page you're reading now, can be accessed as interactive Jupyter notebooks. In fact, you can even grab a link directly to a notebook running interactively on Google Colab - if you want to follow along with this tour, click the link below:" @@ -43,6 +48,7 @@ { "cell_type": "code", "execution_count": null, + "id": "00336b56", "metadata": {}, "outputs": [ { @@ -64,6 +70,7 @@ }, { "cell_type": "markdown", + "id": "8388646e", "metadata": {}, "source": [ "The full docs are available at [fastcore.fast.ai](https://fastcore.fast.ai). The code in the examples and in all fast.ai libraries follow the [fast.ai style guide](https://docs.fast.ai/dev/style.html). In order to support interactive programming, all fast.ai libraries are designed to allow for `import *` to be used safely, particular by ensuring that [`__all__`](https://riptutorial.com/python/example/2894/the---all---special-variable) is defined in all packages. In order to see where a function is from, just type it:" @@ -72,6 +79,7 @@ { "cell_type": "code", "execution_count": null, + "id": "05687ae5", "metadata": {}, "outputs": [ { @@ -91,6 +99,7 @@ }, { "cell_type": "markdown", + "id": "5c324091", "metadata": {}, "source": [ "For more details, including a link to the full documentation and source code, use `doc`, which pops up a window with this information:\n", @@ -106,6 +115,7 @@ }, { "cell_type": "markdown", + "id": "34ec6c68", "metadata": {}, "source": [ "### Testing" @@ -113,6 +123,7 @@ }, { "cell_type": "markdown", + "id": "96f09e79", "metadata": {}, "source": [ "fastcore's testing module is designed to work well with [nbdev](https://nbdev.fast.ai), which is a full literate programming environment built on Jupyter Notebooks. That means that your tests, docs, and code all live together in the same notebook. fastcore and nbdev's approach to testing starts with the premise that all your tests should pass. If one fails, no more tests in a notebook are run.\n", @@ -123,6 +134,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e194af33", "metadata": {}, "outputs": [], "source": [ @@ -131,6 +143,7 @@ }, { "cell_type": "markdown", + "id": "54fb6d99", "metadata": {}, "source": [ "That's an example from the docs for `coll_repr`. As you see, it's not showing you the output directly. Here's what that would look like:" @@ -139,6 +152,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e90b1325", "metadata": {}, "outputs": [ { @@ -158,6 +172,7 @@ }, { "cell_type": "markdown", + "id": "a7cc0ef3", "metadata": {}, "source": [ "So, the test is actually showing you what the output looks like, because if the function call didn't return `'(#1000) [0,1,2,3,4...]'`, then the test would have failed.\n", @@ -170,6 +185,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9630506c", "metadata": {}, "outputs": [], "source": [ @@ -178,6 +194,7 @@ }, { "cell_type": "markdown", + "id": "a5da9f28", "metadata": {}, "source": [ "When a test fails, it prints out information about what was expected:\n", @@ -201,6 +218,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d3192bb1", "metadata": {}, "outputs": [], "source": [ @@ -209,6 +227,7 @@ }, { "cell_type": "markdown", + "id": "80ef89a5", "metadata": {}, "source": [ "You can even test that exceptions are raised:" @@ -217,6 +236,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4d653100", "metadata": {}, "outputs": [], "source": [ @@ -226,6 +246,7 @@ }, { "cell_type": "markdown", + "id": "5604458b", "metadata": {}, "source": [ "...and test that things are printed to stdout:" @@ -234,6 +255,7 @@ { "cell_type": "code", "execution_count": null, + "id": "acfc8b62", "metadata": {}, "outputs": [], "source": [ @@ -242,6 +264,7 @@ }, { "cell_type": "markdown", + "id": "dd9f4fd9", "metadata": {}, "source": [ "### Foundations" @@ -249,6 +272,7 @@ }, { "cell_type": "markdown", + "id": "fdc66d40", "metadata": {}, "source": [ "fast.ai is unusual in that we often use [mixins](https://en.wikipedia.org/wiki/Mixin) in our code. Mixins are widely used in many programming languages, such as Ruby, but not so much in Python. We use mixins to attach new behavior to existing libraries, or to allow modules to add new behavior to our own classes, such as in extension modules. One useful example of a mixin we define is `Path.ls`, which lists a directory and returns an `L` (an extended list class which we'll discuss shortly):" @@ -257,6 +281,7 @@ { "cell_type": "code", "execution_count": null, + "id": "53cd9c94", "metadata": {}, "outputs": [ { @@ -277,6 +302,7 @@ }, { "cell_type": "markdown", + "id": "ae689ed3", "metadata": {}, "source": [ "You can easily add you own mixins with the `patch` [decorator](https://realpython.com/primer-on-python-decorators/), which takes advantage of Python 3 [function annotations](https://www.python.org/dev/peps/pep-3107/#parameters) to say what class to patch:" @@ -285,6 +311,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1180fdcd", "metadata": {}, "outputs": [ { @@ -307,6 +334,7 @@ }, { "cell_type": "markdown", + "id": "065b3f6f", "metadata": {}, "source": [ "We also use `**kwargs` frequently. In python `**kwargs` in a parameter like means \"*put any additional keyword arguments into a dict called `kwargs`*\". Normally, using `kwargs` makes an API quite difficult to work with, because it breaks things like tab-completion and popup lists of signatures. `utils` provides `use_kwargs` and `delegates` to avoid this problem. See our [detailed article on delegation](https://www.fast.ai/2019/08/06/delegation/) on this topic.\n", @@ -317,6 +345,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b242359e", "metadata": {}, "outputs": [ { @@ -344,6 +373,7 @@ }, { "cell_type": "markdown", + "id": "8e379de8", "metadata": {}, "source": [ "Looking at that `ProductPage` example, it's rather verbose and duplicates a lot of attribute names, which can lead to bugs later if you change them only in one place. `fastcore` provides `store_attr` to simplify this common pattern. It also provides `basic_repr` to give simple objects a useful `repr`:" @@ -352,6 +382,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f7223633", "metadata": {}, "outputs": [ { @@ -375,6 +406,7 @@ }, { "cell_type": "markdown", + "id": "820715ec", "metadata": {}, "source": [ "One of the most interesting `fastcore` functions is the `funcs_kwargs` decorator. This allows class behavior to be modified without sub-classing. This can allow folks that aren't familiar with object-oriented programming to customize your class more easily. Here's an example of a class that uses `funcs_kwargs`:" @@ -383,6 +415,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4a0e346e", "metadata": {}, "outputs": [ { @@ -405,6 +438,7 @@ }, { "cell_type": "markdown", + "id": "cbe09f40", "metadata": {}, "source": [ "The `assert not kwargs` above is used to ensure that the user doesn't pass an unknown parameter (i.e one that's not in `_methods`). `fastai` uses `funcs_kwargs` in many places, for instance, you can customize any part of a `DataLoader` by passing your own methods.\n", @@ -416,6 +450,7 @@ }, { "cell_type": "markdown", + "id": "df95dbde", "metadata": {}, "source": [ "### L" @@ -423,6 +458,7 @@ }, { "cell_type": "markdown", + "id": "effd869a", "metadata": {}, "source": [ "Like most languages, Python allows for very concise syntax for some very common types, such as `list`, which can be constructed with `[1,2,3]`. Perl's designer Larry Wall explained the reasoning for this kind of syntax:\n", @@ -435,6 +471,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c2a76a2d", "metadata": {}, "outputs": [ { @@ -454,6 +491,7 @@ }, { "cell_type": "markdown", + "id": "8c2adcc9", "metadata": {}, "source": [ "The first thing to notice is that an `L` object includes in its representation its number of elements; that's the `(#3)` in the output above. If there's more than 10 elements, it will automatically truncate the list:" @@ -462,6 +500,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5987aef4", "metadata": {}, "outputs": [ { @@ -482,6 +521,7 @@ }, { "cell_type": "markdown", + "id": "461ab0e4", "metadata": {}, "source": [ "`L` contains many of the same indexing ideas that NumPy's `array` does, including indexing with a list of indexes, or a boolean mask list:" @@ -490,6 +530,7 @@ { "cell_type": "code", "execution_count": null, + "id": "754c0de2", "metadata": {}, "outputs": [ { @@ -509,6 +550,7 @@ }, { "cell_type": "markdown", + "id": "a1171e2d", "metadata": {}, "source": [ "It also contains other methods used in `array`, such as `L.argwhere`:" @@ -517,6 +559,7 @@ { "cell_type": "code", "execution_count": null, + "id": "bbe0cd97", "metadata": {}, "outputs": [ { @@ -536,6 +579,7 @@ }, { "cell_type": "markdown", + "id": "5192093b", "metadata": {}, "source": [ "As you can see from this example, `fastcore` also includes a number of features that make a functional style of programming easier, such as a full range of boolean functions (e.g `ge`, `gt`, etc) which give the same answer as the functions from Python's `operator` module if given two parameters, but return a [curried function](https://en.wikipedia.org/wiki/Currying) if given one parameter.\n", @@ -546,6 +590,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e00419dc", "metadata": {}, "outputs": [ { @@ -566,21 +611,13 @@ { "cell_type": "code", "execution_count": null, + "id": "75ff9241", "metadata": {}, "outputs": [], "source": [] } ], - "metadata": { - "jupytext": { - "split_at_heading": true - }, - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, + "metadata": {}, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } From 24ae3213e4cad919c54a541ff14fac56002b5a20 Mon Sep 17 00:00:00 2001 From: Rens Date: Sun, 19 Oct 2025 12:01:12 +0200 Subject: [PATCH 133/182] add write_json --- fastcore/_modidx.py | 1 + fastcore/xtras.py | 6 ++++++ nbs/03_xtras.ipynb | 13 +++++++++++++ 3 files changed, 20 insertions(+) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 9f3fb4cc..9913f91f 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -577,6 +577,7 @@ 'fastcore.xtras.Path.read_json': ('xtras.html#path.read_json', 'fastcore/xtras.py'), 'fastcore.xtras.Path.readlines': ('xtras.html#path.readlines', 'fastcore/xtras.py'), 'fastcore.xtras.Path.relpath': ('xtras.html#path.relpath', 'fastcore/xtras.py'), + 'fastcore.xtras.Path.write_json': ('xtras.html#path.write_json', 'fastcore/xtras.py'), 'fastcore.xtras.ReindexCollection': ('xtras.html#reindexcollection', 'fastcore/xtras.py'), 'fastcore.xtras.ReindexCollection.__getitem__': ( 'xtras.html#reindexcollection.__getitem__', 'fastcore/xtras.py'), diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 18ee6f82..06813b05 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -366,6 +366,12 @@ def mk_write(self:Path, data, encoding=None, errors=None, mode=511, uid=-1, gid= self.write_text(data, encoding=encoding, errors=errors) if uid!=-1 or gid!=-1: os.chown(self, uid, gid) +# %% ../nbs/03_xtras.ipynb +@patch +def write_json(self:Path, data, encoding=None, errors=None, mode=511, uid=-1, gid=-1, **kw): + "Same as `dumps`followed by `mk_write`" + self.mk_write(dumps(data,**kw),encoding,errors,mode,uid,gid) + # %% ../nbs/03_xtras.ipynb @patch def relpath(self:Path, start=None): diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 65fc5a87..6b3838d5 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -1237,6 +1237,19 @@ " if uid!=-1 or gid!=-1: os.chown(self, uid, gid)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "@patch\n", + "def write_json(self:Path, data, encoding=None, errors=None, mode=511, uid=-1, gid=-1, **kw):\n", + " \"Same as `dumps`followed by `mk_write`\"\n", + " self.mk_write(dumps(data,**kw),encoding,errors,mode,uid,gid)" + ] + }, { "cell_type": "code", "execution_count": null, From 29e832c4aa03ff91b4c57847a9e899ee3da16db4 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Wed, 29 Oct 2025 15:37:49 +1000 Subject: [PATCH 134/182] fixes #703 --- fastcore/_modidx.py | 40 ++- fastcore/docments.py | 158 ++++++++++- nbs/04_docments.ipynb | 591 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 787 insertions(+), 2 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 9913f91f..e4633086 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -225,7 +225,31 @@ 'fastcore.basics.wrap_class': ('basics.html#wrap_class', 'fastcore/basics.py'), 'fastcore.basics.zip_cycle': ('basics.html#zip_cycle', 'fastcore/basics.py')}, 'fastcore.dispatch': {}, - 'fastcore.docments': { 'fastcore.docments._DocstringExtractor': ('docments.html#_docstringextractor', 'fastcore/docments.py'), + 'fastcore.docments': { 'fastcore.docments.DocmentTbl': ('docments.html#docmenttbl', 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl.__eq__': ('docments.html#docmenttbl.__eq__', 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl.__init__': ('docments.html#docmenttbl.__init__', 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl._columns': ('docments.html#docmenttbl._columns', 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl._hdr_list': ('docments.html#docmenttbl._hdr_list', 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl._repr_markdown_': ( 'docments.html#docmenttbl._repr_markdown_', + 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl._row': ('docments.html#docmenttbl._row', 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl._row_list': ('docments.html#docmenttbl._row_list', 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl.has_docment': ( 'docments.html#docmenttbl.has_docment', + 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl.has_return': ( 'docments.html#docmenttbl.has_return', + 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl.hdr_str': ('docments.html#docmenttbl.hdr_str', 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl.params_str': ( 'docments.html#docmenttbl.params_str', + 'fastcore/docments.py'), + 'fastcore.docments.DocmentTbl.return_str': ( 'docments.html#docmenttbl.return_str', + 'fastcore/docments.py'), + 'fastcore.docments.MarkdownRenderer': ('docments.html#markdownrenderer', 'fastcore/docments.py'), + 'fastcore.docments.MarkdownRenderer._repr_markdown_': ( 'docments.html#markdownrenderer._repr_markdown_', + 'fastcore/docments.py'), + 'fastcore.docments.ShowDocRenderer': ('docments.html#showdocrenderer', 'fastcore/docments.py'), + 'fastcore.docments.ShowDocRenderer.__init__': ( 'docments.html#showdocrenderer.__init__', + 'fastcore/docments.py'), + 'fastcore.docments._DocstringExtractor': ('docments.html#_docstringextractor', 'fastcore/docments.py'), 'fastcore.docments._DocstringExtractor.__init__': ( 'docments.html#_docstringextractor.__init__', 'fastcore/docments.py'), 'fastcore.docments._DocstringExtractor.visit_ClassDef': ( 'docments.html#_docstringextractor.visit_classdef', @@ -234,17 +258,31 @@ 'fastcore/docments.py'), 'fastcore.docments._DocstringExtractor.visit_Module': ( 'docments.html#_docstringextractor.visit_module', 'fastcore/docments.py'), + 'fastcore.docments._bold': ('docments.html#_bold', 'fastcore/docments.py'), 'fastcore.docments._clean_comment': ('docments.html#_clean_comment', 'fastcore/docments.py'), 'fastcore.docments._docments': ('docments.html#_docments', 'fastcore/docments.py'), + 'fastcore.docments._docstring': ('docments.html#_docstring', 'fastcore/docments.py'), + 'fastcore.docments._escape_markdown': ('docments.html#_escape_markdown', 'fastcore/docments.py'), + 'fastcore.docments._ext_link': ('docments.html#_ext_link', 'fastcore/docments.py'), + 'fastcore.docments._f_name': ('docments.html#_f_name', 'fastcore/docments.py'), + 'fastcore.docments._fmt_anno': ('docments.html#_fmt_anno', 'fastcore/docments.py'), + 'fastcore.docments._fmt_sig': ('docments.html#_fmt_sig', 'fastcore/docments.py'), + 'fastcore.docments._fullname': ('docments.html#_fullname', 'fastcore/docments.py'), 'fastcore.docments._get_comment': ('docments.html#_get_comment', 'fastcore/docments.py'), 'fastcore.docments._get_full': ('docments.html#_get_full', 'fastcore/docments.py'), 'fastcore.docments._get_params': ('docments.html#_get_params', 'fastcore/docments.py'), 'fastcore.docments._get_property_name': ('docments.html#_get_property_name', 'fastcore/docments.py'), + 'fastcore.docments._ital_first': ('docments.html#_ital_first', 'fastcore/docments.py'), + 'fastcore.docments._list2row': ('docments.html#_list2row', 'fastcore/docments.py'), + 'fastcore.docments._maybe_nm': ('docments.html#_maybe_nm', 'fastcore/docments.py'), 'fastcore.docments._merge_doc': ('docments.html#_merge_doc', 'fastcore/docments.py'), 'fastcore.docments._merge_docs': ('docments.html#_merge_docs', 'fastcore/docments.py'), + 'fastcore.docments._non_empty_keys': ('docments.html#_non_empty_keys', 'fastcore/docments.py'), 'fastcore.docments._param_locs': ('docments.html#_param_locs', 'fastcore/docments.py'), 'fastcore.docments._parses': ('docments.html#_parses', 'fastcore/docments.py'), + 'fastcore.docments._show_param': ('docments.html#_show_param', 'fastcore/docments.py'), 'fastcore.docments._tokens': ('docments.html#_tokens', 'fastcore/docments.py'), + 'fastcore.docments._wrap_sig': ('docments.html#_wrap_sig', 'fastcore/docments.py'), 'fastcore.docments.docments': ('docments.html#docments', 'fastcore/docments.py'), 'fastcore.docments.docstring': ('docments.html#docstring', 'fastcore/docments.py'), 'fastcore.docments.extract_docstrings': ('docments.html#extract_docstrings', 'fastcore/docments.py'), diff --git a/fastcore/docments.py b/fastcore/docments.py index 9b18d0ba..6f9425a6 100644 --- a/fastcore/docments.py +++ b/fastcore/docments.py @@ -16,11 +16,12 @@ from .utils import * from .meta import delegates from . import docscrape +from textwrap import fill from inspect import isclass,getdoc # %% auto 0 __all__ = ['empty', 'docstring', 'parse_docstring', 'isdataclass', 'get_dataclass_source', 'get_source', 'get_name', 'qual_name', - 'docments', 'sig2str', 'extract_docstrings'] + 'docments', 'sig2str', 'extract_docstrings', 'DocmentTbl', 'ShowDocRenderer', 'MarkdownRenderer'] # %% ../nbs/04_docments.ipynb def docstring(sym): @@ -247,3 +248,158 @@ def extract_docstrings(code): extractor = _DocstringExtractor() extractor.visit(ast.parse(code)) return extractor.docstrings + +# %% ../nbs/04_docments.ipynb +def _non_empty_keys(d:dict): return L([k for k,v in d.items() if v != inspect._empty]) +def _bold(s): return f'**{s}**' if s.strip() else s + +# %% ../nbs/04_docments.ipynb +def _escape_markdown(s): + for c in '|^': s = re.sub(rf'\\?\{c}', rf'\{c}', s) + return s.replace('\n', '
') + +# %% ../nbs/04_docments.ipynb +def _maybe_nm(o): + if (o == inspect._empty): return '' + else: return o.__name__ if hasattr(o, '__name__') else _escape_markdown(str(o)) + +# %% ../nbs/04_docments.ipynb +def _list2row(l:list): return '| '+' | '.join([_maybe_nm(o) for o in l]) + ' |' + +# %% ../nbs/04_docments.ipynb +class DocmentTbl: + # this is the column order we want these items to appear + _map = {'anno':'Type', 'default':'Default', 'docment':'Details'} + + def __init__(self, obj, verbose=True, returns=True): + "Compute the docment table string" + self.verbose = verbose + self.returns = False if isdataclass(obj) else returns + try: self.params = L(signature_ex(obj, eval_str=True).parameters.keys()) + except (ValueError,TypeError): self.params=[] + try: _dm = docments(obj, full=True, returns=returns) + except: _dm = {} + if 'self' in _dm: del _dm['self'] + for d in _dm.values(): d['docment'] = ifnone(d['docment'], inspect._empty) + self.dm = _dm + + @property + def _columns(self): + "Compute the set of fields that have at least one non-empty value so we don't show tables empty columns" + cols = set(flatten(L(self.dm.values()).filter().map(_non_empty_keys))) + candidates = self._map if self.verbose else {'docment': 'Details'} + return {k:v for k,v in candidates.items() if k in cols} + + @property + def has_docment(self): return 'docment' in self._columns and self._row_list + + @property + def has_return(self): return self.returns and bool(_non_empty_keys(self.dm.get('return', {}))) + + def _row(self, nm, props): + "unpack data for single row to correspond with column names." + return [nm] + [props[c] for c in self._columns] + + @property + def _row_list(self): + "unpack data for all rows." + ordered_params = [(p, self.dm[p]) for p in self.params if p != 'self' and p in self.dm] + return L([self._row(nm, props) for nm,props in ordered_params]) + + @property + def _hdr_list(self): return [' '] + [_bold(l) for l in L(self._columns.values())] + + @property + def hdr_str(self): + "The markdown string for the header portion of the table" + md = _list2row(self._hdr_list) + return md + '\n' + _list2row(['-' * len(l) for l in self._hdr_list]) + + @property + def params_str(self): + "The markdown string for the parameters portion of the table." + return '\n'.join(self._row_list.map(_list2row)) + + @property + def return_str(self): + "The markdown string for the returns portion of the table." + return _list2row(['**Returns**']+[_bold(_maybe_nm(self.dm['return'][c])) for c in self._columns]) + + def _repr_markdown_(self): + if not self.has_docment: return '' + _tbl = [self.hdr_str, self.params_str] + if self.has_return: _tbl.append(self.return_str) + return '\n'.join(_tbl) + + def __eq__(self,other): return self.__str__() == str(other).strip() + + __str__ = _repr_markdown_ + __repr__ = basic_repr() + +# %% ../nbs/04_docments.ipynb +def _docstring(sym): + npdoc = parse_docstring(sym) + return '\n\n'.join([npdoc['Summary'], npdoc['Extended']]).strip() + +# %% ../nbs/04_docments.ipynb +def _fullname(o): + module,name = getattr(o, "__module__", None),qual_name(o) + return name if module is None or module in ('__main__','builtins') else module + '.' + name + +class ShowDocRenderer: + def __init__(self, sym, name:str|None=None, title_level:int=3): + "Show documentation for `sym`" + sym = getattr(sym, '__wrapped__', sym) + sym = getattr(sym, 'fget', None) or getattr(sym, 'fset', None) or sym + store_attr() + self.nm = name or qual_name(sym) + self.isfunc = inspect.isfunction(sym) + try: self.sig = signature_ex(sym, eval_str=True) + except (ValueError,TypeError): self.sig = None + self.docs = _docstring(sym) + self.dm = DocmentTbl(sym) + self.fn = _fullname(sym) + + __repr__ = basic_repr() + +# %% ../nbs/04_docments.ipynb +def _f_name(o): return f'' if isinstance(o, FunctionType) else None +def _fmt_anno(o): return inspect.formatannotation(o).strip("'").replace(' ','') + +def _show_param(param): + "Like `Parameter.__str__` except removes: quotes in annos, spaces, ids in reprs" + kind,res,anno,default = param.kind,param._name,param._annotation,param._default + kind = '*' if kind==inspect._VAR_POSITIONAL else '**' if kind==inspect._VAR_KEYWORD else '' + res = kind+res + if anno is not inspect._empty: res += f':{_f_name(anno) or _fmt_anno(anno)}' + if default is not inspect._empty: res += f'={_f_name(default) or repr(default)}' + return res + +# %% ../nbs/04_docments.ipynb +def _fmt_sig(sig): + if sig is None: return '' + p = {k:v for k,v in sig.parameters.items()} + _params = [_show_param(p[k]) for k in p.keys() if k != 'self'] + return "(" + ', '.join(_params) + ")" + +def _wrap_sig(s): + "wrap a signature to appear on multiple lines if necessary." + pad = '> ' + ' ' * 5 + indent = pad + ' ' * (s.find('(') + 1) + return fill(s, width=80, initial_indent=pad, subsequent_indent=indent) + +def _ital_first(s:str): + "Surround first line with * for markdown italics, preserving leading spaces" + return re.sub(r'^(\s*)(.+)', r'\1*\2*', s, count=1) + +# %% ../nbs/04_docments.ipynb +def _ext_link(url, txt, xtra=""): return f'[{txt}]({url}){{target="_blank" {xtra}}}' + +class MarkdownRenderer(ShowDocRenderer): + "Markdown renderer for `show_doc`" + def _repr_markdown_(self): + doc = _wrap_sig(f"{self.nm} {_fmt_sig(self.sig)}") if self.sig else '' + if self.docs: doc += f"\n\n{_ital_first(self.docs)}" + if self.dm.has_docment: doc += f"\n\n{self.dm}" + return doc + __repr__=__str__=_repr_markdown_ diff --git a/nbs/04_docments.ipynb b/nbs/04_docments.ipynb index 143827c2..c597ae87 100644 --- a/nbs/04_docments.ipynb +++ b/nbs/04_docments.ipynb @@ -38,6 +38,7 @@ "from fastcore.utils import *\n", "from fastcore.meta import delegates\n", "from fastcore import docscrape\n", + "from textwrap import fill\n", "from inspect import isclass,getdoc" ] }, @@ -1257,6 +1258,596 @@ "test_eq(extract_docstrings(sample_code), exp)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Rendering docment Tables" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Render nicely formatted tables that shows `docments` for any function or method. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _non_empty_keys(d:dict): return L([k for k,v in d.items() if v != inspect._empty])\n", + "def _bold(s): return f'**{s}**' if s.strip() else s" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _escape_markdown(s):\n", + " for c in '|^': s = re.sub(rf'\\\\?\\{c}', rf'\\{c}', s)\n", + " return s.replace('\\n', '
')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "test_eq(_escape_markdown('|'), '\\|')\n", + "test_eq(_escape_markdown('\\|'), '\\|')\n", + "test_eq(_escape_markdown(' ^[_'), ' \\^[_') # footnotes\n", + "test_eq(_escape_markdown('foo ^[_'), 'foo \\^[_')\n", + "test_eq(_escape_markdown(' \\^[_'), ' \\^[_') #if it is already escaped leave it alone\n", + "test_eq(_escape_markdown('a long\\nsentence'), 'a long
sentence')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _maybe_nm(o):\n", + " if (o == inspect._empty): return ''\n", + " else: return o.__name__ if hasattr(o, '__name__') else _escape_markdown(str(o))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "test_eq(_maybe_nm(list), 'list')\n", + "test_eq(_maybe_nm('fastai'), 'fastai')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _list2row(l:list): return '| '+' | '.join([_maybe_nm(o) for o in l]) + ' |'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "test_eq(_list2row(['Hamel', 'Jeremy']), '| Hamel | Jeremy |')\n", + "test_eq(_list2row([inspect._empty, bool, 'foo']), '| | bool | foo |')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "class DocmentTbl:\n", + " # this is the column order we want these items to appear\n", + " _map = {'anno':'Type', 'default':'Default', 'docment':'Details'}\n", + "\n", + " def __init__(self, obj, verbose=True, returns=True):\n", + " \"Compute the docment table string\"\n", + " self.verbose = verbose\n", + " self.returns = False if isdataclass(obj) else returns\n", + " try: self.params = L(signature_ex(obj, eval_str=True).parameters.keys())\n", + " except (ValueError,TypeError): self.params=[]\n", + " try: _dm = docments(obj, full=True, returns=returns)\n", + " except: _dm = {}\n", + " if 'self' in _dm: del _dm['self']\n", + " for d in _dm.values(): d['docment'] = ifnone(d['docment'], inspect._empty)\n", + " self.dm = _dm\n", + "\n", + " @property\n", + " def _columns(self):\n", + " \"Compute the set of fields that have at least one non-empty value so we don't show tables empty columns\"\n", + " cols = set(flatten(L(self.dm.values()).filter().map(_non_empty_keys)))\n", + " candidates = self._map if self.verbose else {'docment': 'Details'}\n", + " return {k:v for k,v in candidates.items() if k in cols}\n", + "\n", + " @property\n", + " def has_docment(self): return 'docment' in self._columns and self._row_list\n", + "\n", + " @property\n", + " def has_return(self): return self.returns and bool(_non_empty_keys(self.dm.get('return', {})))\n", + "\n", + " def _row(self, nm, props):\n", + " \"unpack data for single row to correspond with column names.\"\n", + " return [nm] + [props[c] for c in self._columns]\n", + "\n", + " @property\n", + " def _row_list(self):\n", + " \"unpack data for all rows.\"\n", + " ordered_params = [(p, self.dm[p]) for p in self.params if p != 'self' and p in self.dm]\n", + " return L([self._row(nm, props) for nm,props in ordered_params])\n", + "\n", + " @property\n", + " def _hdr_list(self): return [' '] + [_bold(l) for l in L(self._columns.values())]\n", + "\n", + " @property\n", + " def hdr_str(self):\n", + " \"The markdown string for the header portion of the table\"\n", + " md = _list2row(self._hdr_list)\n", + " return md + '\\n' + _list2row(['-' * len(l) for l in self._hdr_list])\n", + "\n", + " @property\n", + " def params_str(self):\n", + " \"The markdown string for the parameters portion of the table.\"\n", + " return '\\n'.join(self._row_list.map(_list2row))\n", + "\n", + " @property\n", + " def return_str(self):\n", + " \"The markdown string for the returns portion of the table.\"\n", + " return _list2row(['**Returns**']+[_bold(_maybe_nm(self.dm['return'][c])) for c in self._columns])\n", + "\n", + " def _repr_markdown_(self):\n", + " if not self.has_docment: return ''\n", + " _tbl = [self.hdr_str, self.params_str]\n", + " if self.has_return: _tbl.append(self.return_str)\n", + " return '\\n'.join(_tbl)\n", + "\n", + " def __eq__(self,other): return self.__str__() == str(other).strip()\n", + "\n", + " __str__ = _repr_markdown_\n", + " __repr__ = basic_repr()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`DocmentTbl` can render a markdown table showing `docments` if appropriate. This is an example of how a `docments` table will render for a function:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "| | **Type** | **Default** | **Details** |\n", + "| -- | -------- | ----------- | ----------- |\n", + "| a | | | description of param a |\n", + "| b | bool | True | description of param b |\n", + "| c | str | None | |\n", + "| **Returns** | **int** | | |" + ], + "text/plain": [ + "DocmentTbl(verbose=True, returns=True, params=['a', 'b', 'c'], dm={'a': {'docment': 'description of param a', 'anno': , 'default': }, 'b': {'docment': 'description of param b', 'anno': , 'default': True}, 'c': {'docment': , 'anno': , 'default': None}, 'return': {'docment': , 'anno': , 'default': }})" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def _f(a, # description of param a\n", + " b=True, # description of param b\n", + " c:str=None\n", + " ) -> int: ...\n", + "\n", + "_dm = DocmentTbl(_f)\n", + "_dm" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "_exp_res=\"\"\"\n", + "| | **Type** | **Default** | **Details** |\n", + "| -- | -------- | ----------- | ----------- |\n", + "| a | | | description of param a |\n", + "| b | bool | True | description of param b |\n", + "| c | str | None | |\n", + "| **Returns** | **int** | | |\n", + "\"\"\"\n", + "\n", + "test_eq(_dm, _exp_res)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If one column in the table has no information, for example because there are no default values, that column will not be shown. In the below example, the **Default** column, will not be shown. Additionally, if the return of the function is not annotated the **Returns** row will not be rendered:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "| | **Details** |\n", + "| -- | ----------- |\n", + "| a | |\n", + "| b | param b |\n", + "| c | param c |" + ], + "text/plain": [ + "DocmentTbl(verbose=True, returns=True, params=['a', 'b', 'c'], dm={'a': {'docment': , 'anno': , 'default': }, 'b': {'docment': 'param b', 'anno': , 'default': }, 'c': {'docment': 'param c', 'anno': , 'default': }, 'return': {'docment': , 'anno': , 'default': }})" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def _f(a,\n", + " b, #param b\n", + " c #param c\n", + " ): ...\n", + "\n", + "_dm2 = DocmentTbl(_f)\n", + "_dm2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "_exp_res2 = \"\"\"\n", + "| | **Details** |\n", + "| -- | ----------- |\n", + "| a | |\n", + "| b | param b |\n", + "| c | param c |\n", + "\"\"\"\n", + "\n", + "test_eq(_dm2, _exp_res2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`DocmentTbl` also works on classes. By default, the `__init__` will be rendered:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class _Test:\n", + " def __init__(self,\n", + " a, # description of param a\n", + " b=True, # description of param b\n", + " c:str=None):\n", + " ...\n", + "\n", + " def foo(self,\n", + " c:int, # description of param c\n", + " d=True, # description of param d\n", + " ):\n", + " ..." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "| | **Type** | **Default** | **Details** |\n", + "| -- | -------- | ----------- | ----------- |\n", + "| a | | | description of param a |\n", + "| b | bool | True | description of param b |\n", + "| c | str | None | |" + ], + "text/plain": [ + "DocmentTbl(verbose=True, returns=True, params=['a', 'b', 'c'], dm={'a': {'docment': 'description of param a', 'anno': , 'default': }, 'b': {'docment': 'description of param b', 'anno': , 'default': True}, 'c': {'docment': , 'anno': , 'default': None}, 'return': {'docment': , 'anno': , 'default': }})" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DocmentTbl(_Test)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also pass a method to be rendered as well:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "| | **Type** | **Default** | **Details** |\n", + "| -- | -------- | ----------- | ----------- |\n", + "| c | int | | description of param c |\n", + "| d | bool | True | description of param d |" + ], + "text/plain": [ + "DocmentTbl(verbose=True, returns=True, params=['self', 'c', 'd'], dm={'c': {'docment': 'description of param c', 'anno': , 'default': }, 'd': {'docment': 'description of param d', 'anno': , 'default': True}, 'return': {'docment': , 'anno': , 'default': }})" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DocmentTbl(_Test.foo)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "_exp_res3 = \"\"\"\n", + "| | **Type** | **Default** | **Details** |\n", + "| -- | -------- | ----------- | ----------- |\n", + "| c | int | | description of param c |\n", + "| d | bool | True | description of param d |\n", + "\"\"\"\n", + "\n", + "test_eq(DocmentTbl(_Test.foo), _exp_res3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Documentation For An Object" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Render the signature as well as the `docments` to show complete documentation for an object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _docstring(sym):\n", + " npdoc = parse_docstring(sym)\n", + " return '\\n\\n'.join([npdoc['Summary'], npdoc['Extended']]).strip()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _fullname(o):\n", + " module,name = getattr(o, \"__module__\", None),qual_name(o)\n", + " return name if module is None or module in ('__main__','builtins') else module + '.' + name\n", + "\n", + "class ShowDocRenderer:\n", + " def __init__(self, sym, name:str|None=None, title_level:int=3):\n", + " \"Show documentation for `sym`\"\n", + " sym = getattr(sym, '__wrapped__', sym)\n", + " sym = getattr(sym, 'fget', None) or getattr(sym, 'fset', None) or sym\n", + " store_attr()\n", + " self.nm = name or qual_name(sym)\n", + " self.isfunc = inspect.isfunction(sym)\n", + " try: self.sig = signature_ex(sym, eval_str=True)\n", + " except (ValueError,TypeError): self.sig = None\n", + " self.docs = _docstring(sym)\n", + " self.dm = DocmentTbl(sym)\n", + " self.fn = _fullname(sym)\n", + "\n", + " __repr__ = basic_repr()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _f_name(o): return f'' if isinstance(o, FunctionType) else None\n", + "def _fmt_anno(o): return inspect.formatannotation(o).strip(\"'\").replace(' ','')\n", + "\n", + "def _show_param(param):\n", + " \"Like `Parameter.__str__` except removes: quotes in annos, spaces, ids in reprs\"\n", + " kind,res,anno,default = param.kind,param._name,param._annotation,param._default\n", + " kind = '*' if kind==inspect._VAR_POSITIONAL else '**' if kind==inspect._VAR_KEYWORD else ''\n", + " res = kind+res\n", + " if anno is not inspect._empty: res += f':{_f_name(anno) or _fmt_anno(anno)}'\n", + " if default is not inspect._empty: res += f'={_f_name(default) or repr(default)}'\n", + " return res" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "def _func(): pass\n", + "p = Parameter('foo', Parameter.POSITIONAL_OR_KEYWORD, default=_func, annotation='Callable')\n", + "test_eq(_show_param(p), 'foo:Callable=')\n", + "p = p.replace(annotation=_func)\n", + "test_eq(_show_param(p), 'foo:=')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _fmt_sig(sig):\n", + " if sig is None: return ''\n", + " p = {k:v for k,v in sig.parameters.items()}\n", + " _params = [_show_param(p[k]) for k in p.keys() if k != 'self']\n", + " return \"(\" + ', '.join(_params) + \")\"\n", + "\n", + "def _wrap_sig(s):\n", + " \"wrap a signature to appear on multiple lines if necessary.\"\n", + " pad = '> ' + ' ' * 5\n", + " indent = pad + ' ' * (s.find('(') + 1)\n", + " return fill(s, width=80, initial_indent=pad, subsequent_indent=indent)\n", + "\n", + "def _ital_first(s:str):\n", + " \"Surround first line with * for markdown italics, preserving leading spaces\"\n", + " return re.sub(r'^(\\s*)(.+)', r'\\1*\\2*', s, count=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|hide\n", + "def _long_f(a_param, b_param=True, c_param:str='Some quite long value', d:int=2, e:bool=False):\n", + " \"A docstring\"\n", + " ...\n", + "\n", + "_res = \"> (a_param, b_param=True, c_param:str='Some quite long value', d:int=2,\\n> e:bool=False)\"\n", + "_sig = _fmt_sig(signature_ex(_long_f, eval_str=True))\n", + "test_eq(_wrap_sig(_sig), _res)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "def _ext_link(url, txt, xtra=\"\"): return f'[{txt}]({url}){{target=\"_blank\" {xtra}}}'\n", + "\n", + "class MarkdownRenderer(ShowDocRenderer):\n", + " \"Markdown renderer for `show_doc`\"\n", + " def _repr_markdown_(self):\n", + " doc = _wrap_sig(f\"{self.nm} {_fmt_sig(self.sig)}\") if self.sig else ''\n", + " if self.docs: doc += f\"\\n\\n{_ital_first(self.docs)}\"\n", + " if self.dm.has_docment: doc += f\"\\n\\n{self.dm}\"\n", + " return doc\n", + " __repr__=__str__=_repr_markdown_" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "> _f (a, b:int, c:str='foo')\n", + "\n", + "*Do a thing*\n", + "\n", + "| | **Type** | **Default** | **Details** |\n", + "| -- | -------- | ----------- | ----------- |\n", + "| a | | | |\n", + "| b | int | | param b |\n", + "| c | str | foo | param c |\n", + "| **Returns** | **str** | | **Result of doing it** |" + ], + "text/plain": [ + "> _f (a, b:int, c:str='foo')\n", + "\n", + "*Do a thing*\n", + "\n", + "| | **Type** | **Default** | **Details** |\n", + "| -- | -------- | ----------- | ----------- |\n", + "| a | | | |\n", + "| b | int | | param b |\n", + "| c | str | foo | param c |\n", + "| **Returns** | **str** | | **Result of doing it** |" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def _f(a,\n", + " b:int, #param b\n", + " c:str='foo' #param c\n", + " )->str: # Result of doing it\n", + " \"Do a thing\"\n", + " ...\n", + "\n", + "MarkdownRenderer(_f)" + ] + }, { "cell_type": "markdown", "metadata": {}, From fdaceed2ba10ff422bca87ec3391be48fb94a24a Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Wed, 29 Oct 2025 15:38:25 +1000 Subject: [PATCH 135/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8de9f577..c295eea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.8.14 + +### New Features + +- Add docments renderers (moved from nbdev) ([#703](https://github.com/AnswerDotAI/fastcore/issues/703)) +- add `write_json` ([#702](https://github.com/AnswerDotAI/fastcore/pull/702)), thanks to [@RensDimmendaal](https://github.com/RensDimmendaal) + + ## 1.8.13 ### New Features From e9caf9515a3eff401b170c0df651d37d45abb585 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Wed, 29 Oct 2025 15:38:47 +1000 Subject: [PATCH 136/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index d6578c97..c01b838e 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.14" +__version__ = "1.8.15" diff --git a/settings.ini b/settings.ini index 56034486..e7ec0620 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.14 +version = 1.8.15 min_python = 3.10 audience = Developers language = English From 57be4180ecba90912e1e5bdbcdc7027d71082fe9 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Wed, 5 Nov 2025 06:19:28 +1000 Subject: [PATCH 137/182] fixes #704 --- fastcore/basics.py | 2 +- nbs/01_basics.ipynb | 32 +++++++++----------------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/fastcore/basics.py b/fastcore/basics.py index ffde55bf..1117dc4b 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -284,7 +284,7 @@ class AttrDict(dict): def __getattr__(self,k): return self[k] if k in self else stop(AttributeError(k)) def __setattr__(self, k, v): (self.__setitem__,super().__setattr__)[k[0]=='_'](k,v) def __dir__(self): return super().__dir__() + list(self.keys()) - def _repr_markdown_(self): return f'```json\n{pprint.pformat(self, indent=2)}\n```' + def _repr_markdown_(self): return f'```python\n{pprint.pformat(self, indent=2)}\n```' def copy(self): return AttrDict(**self) # %% ../nbs/01_basics.ipynb diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index c1742eac..93703444 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -736,12 +736,6 @@ "*Dynamically create a class, optionally inheriting from `sup`, containing `fld_names`*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### get_class\n", - "\n", "> get_class (nm, *fld_names, sup=None, doc=None, funcs=None, anno=None,\n", "> **flds)\n", "\n", @@ -936,12 +930,6 @@ "*Context manager to ignore exceptions*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L158){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### ignore_exceptions\n", - "\n", "> ignore_exceptions ()\n", "\n", "*Context manager to ignore exceptions*" @@ -1105,10 +1093,6 @@ "*Do nothing*" ], "text/plain": [ - "---\n", - "\n", - "### noop\n", - "\n", "> noop (x=None, *args, **kwargs)\n", "\n", "*Do nothing*" @@ -1152,10 +1136,6 @@ "*Do nothing (method)*" ], "text/plain": [ - "---\n", - "\n", - "### noops\n", - "\n", "> noops (x=None, *args, **kwargs)\n", "\n", "*Do nothing (method)*" @@ -1675,7 +1655,7 @@ " def __getattr__(self,k): return self[k] if k in self else stop(AttributeError(k))\n", " def __setattr__(self, k, v): (self.__setitem__,super().__setattr__)[k[0]=='_'](k,v)\n", " def __dir__(self): return super().__dir__() + list(self.keys())\n", - " def _repr_markdown_(self): return f'```json\\n{pprint.pformat(self, indent=2)}\\n```'\n", + " def _repr_markdown_(self): return f'```python\\n{pprint.pformat(self, indent=2)}\\n```'\n", " def copy(self): return AttrDict(**self)" ] }, @@ -1716,7 +1696,7 @@ { "data": { "text/markdown": [ - "```json\n", + "```python\n", "{ 'a': 1,\n", " 'b': {'c': 1, 'd': 2},\n", " 'c': {'c': 1, 'd': 2},\n", @@ -7820,7 +7800,13 @@ "source": [] } ], - "metadata": {}, + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, "nbformat": 4, "nbformat_minor": 5 } From 36547822d7665507b63cda6b3bd219a5da239f57 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Wed, 5 Nov 2025 06:20:07 +1000 Subject: [PATCH 138/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c295eea9..f7f71d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.8.15 + +### Bugs Squashed + +- Incorrect json block in AttrDict ([#704](https://github.com/AnswerDotAI/fastcore/issues/704)) + + ## 1.8.14 ### New Features From 60c02e08d0e9afeab2e4842256f513aab9735df1 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Wed, 5 Nov 2025 06:20:20 +1000 Subject: [PATCH 139/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index c01b838e..15f0ae65 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.15" +__version__ = "1.8.16" diff --git a/settings.ini b/settings.ini index e7ec0620..7d1053f1 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.15 +version = 1.8.16 min_python = 3.10 audience = Developers language = English From 52143e0dd1f516d7fa86f65227c280623618f6c0 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 8 Nov 2025 09:24:21 +1000 Subject: [PATCH 140/182] fixes #705 --- fastcore/_modidx.py | 1 + fastcore/basics.py | 1 + nbs/01_basics.ipynb | 95 +++++++++++---------------------------------- 3 files changed, 24 insertions(+), 73 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index e4633086..56f6b50b 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -42,6 +42,7 @@ 'fastcore.basics.NotStr.__bool__': ('basics.html#notstr.__bool__', 'fastcore/basics.py'), 'fastcore.basics.NotStr.__contains__': ('basics.html#notstr.__contains__', 'fastcore/basics.py'), 'fastcore.basics.NotStr.__eq__': ('basics.html#notstr.__eq__', 'fastcore/basics.py'), + 'fastcore.basics.NotStr.__getitem__': ('basics.html#notstr.__getitem__', 'fastcore/basics.py'), 'fastcore.basics.NotStr.__hash__': ('basics.html#notstr.__hash__', 'fastcore/basics.py'), 'fastcore.basics.NotStr.__init__': ('basics.html#notstr.__init__', 'fastcore/basics.py'), 'fastcore.basics.NotStr.__iter__': ('basics.html#notstr.__iter__', 'fastcore/basics.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index 1117dc4b..db8106c5 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -1152,6 +1152,7 @@ def __hash__(self): return hash(self.s) def __bool__(self): return bool(self.s) def __contains__(self, b): return b in self.s def __iter__(self): return iter(self.s) + def __getitem__(self, i): return NotStr(self.s[i]) # %% ../nbs/01_basics.ipynb class PrettyString(str): diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 93703444..1f42653f 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -3119,7 +3119,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L525){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L542){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### GetAttr\n", "\n", @@ -3128,12 +3128,6 @@ "*Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L525){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### GetAttr\n", - "\n", "> GetAttr ()\n", "\n", "*Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`*" @@ -5054,7 +5048,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L861){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L878){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### fastuple\n", "\n", @@ -5063,12 +5057,6 @@ "*A `tuple` with elementwise ops and more friendly __init__ behavior*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L861){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### fastuple\n", - "\n", "> fastuple (x=None, *rest)\n", "\n", "*A `tuple` with elementwise ops and more friendly __init__ behavior*" @@ -5144,7 +5132,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L880){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L897){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### fastuple.add\n", "\n", @@ -5153,12 +5141,6 @@ "*`+` is already defined in `tuple` for concat, so use `add` instead*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L880){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "##### fastuple.add\n", - "\n", "> fastuple.add (*args)\n", "\n", "*`+` is already defined in `tuple` for concat, so use `add` instead*" @@ -5196,7 +5178,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L876){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L893){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### fastuple.mul\n", "\n", @@ -5205,12 +5187,6 @@ "*`*` is already defined in `tuple` for replicating, so use `mul` instead*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L876){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "##### fastuple.mul\n", - "\n", "> fastuple.mul (*args)\n", "\n", "*`*` is already defined in `tuple` for replicating, so use `mul` instead*" @@ -5369,7 +5345,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L907){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L924){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### bind\n", "\n", @@ -5378,12 +5354,6 @@ "*Same as `partial`, except you can use `arg0` `arg1` etc param placeholders*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L907){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### bind\n", - "\n", "> bind (func, *pargs, **pkwargs)\n", "\n", "*Same as `partial`, except you can use `arg0` `arg1` etc param placeholders*" @@ -6478,23 +6448,17 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1080){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1097){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ImportEnum\n", "\n", - "> ImportEnum (value, names=None, module=None, qualname=None, type=None,\n", + "> ImportEnum (new_class_name, names, module=None, qualname=None, type=None,\n", "> start=1, boundary=None)\n", "\n", "*An `Enum` that can have its values imported*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1080){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### ImportEnum\n", - "\n", - "> ImportEnum (value, names=None, module=None, qualname=None, type=None,\n", + "> ImportEnum (new_class_name, names, module=None, qualname=None, type=None,\n", "> start=1, boundary=None)\n", "\n", "*An `Enum` that can have its values imported*" @@ -6546,23 +6510,17 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1088){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1105){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### StrEnum\n", "\n", - "> StrEnum (value, names=None, module=None, qualname=None, type=None,\n", + "> StrEnum (new_class_name, names, module=None, qualname=None, type=None,\n", "> start=1, boundary=None)\n", "\n", "*An `ImportEnum` that behaves like a `str`*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1088){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### StrEnum\n", - "\n", - "> StrEnum (value, names=None, module=None, qualname=None, type=None,\n", + "> StrEnum (new_class_name, names, module=None, qualname=None, type=None,\n", "> start=1, boundary=None)\n", "\n", "*An `ImportEnum` that behaves like a `str`*" @@ -6614,23 +6572,17 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1098){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1115){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ValEnum\n", "\n", - "> ValEnum (value, names=None, module=None, qualname=None, type=None,\n", + "> ValEnum (new_class_name, names, module=None, qualname=None, type=None,\n", "> start=1, boundary=None)\n", "\n", "*An `ImportEnum` that stringifies using values*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1098){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### ValEnum\n", - "\n", - "> ValEnum (value, names=None, module=None, qualname=None, type=None,\n", + "> ValEnum (new_class_name, names, module=None, qualname=None, type=None,\n", "> start=1, boundary=None)\n", "\n", "*An `ImportEnum` that stringifies using values*" @@ -6706,7 +6658,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1103){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1120){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### Stateful\n", "\n", @@ -6715,12 +6667,6 @@ "*A base class/mixin for objects that should not serialize all their state*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1103){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### Stateful\n", - "\n", "> Stateful (*args, **kwargs)\n", "\n", "*A base class/mixin for objects that should not serialize all their state*" @@ -6816,7 +6762,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8b4b2755", + "id": "5094ae23", "metadata": {}, "outputs": [], "source": [ @@ -6835,13 +6781,14 @@ " def __hash__(self): return hash(self.s)\n", " def __bool__(self): return bool(self.s)\n", " def __contains__(self, b): return b in self.s\n", - " def __iter__(self): return iter(self.s)" + " def __iter__(self): return iter(self.s)\n", + " def __getitem__(self, i): return NotStr(self.s[i])" ] }, { "cell_type": "code", "execution_count": null, - "id": "1c296d2b", + "id": "cff43774", "metadata": {}, "outputs": [], "source": [ @@ -6849,7 +6796,9 @@ "assert not isinstance(s, str)\n", "test_eq(s, 'hello')\n", "test_eq(s*2, 'hellohello')\n", - "test_eq(len(s), 5)" + "test_eq(len(s), 5)\n", + "test_eq(s[:2], \"he\")\n", + "test_eq(s[2], \"l\")" ] }, { From 0736de9cc7c9fed2a8492ebd966f4096dd317dcc Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 8 Nov 2025 09:24:31 +1000 Subject: [PATCH 141/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f71d3d..12f7ba58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.8.16 + +### New Features + +- Add `__getitem__` to `NotStr` ([#705](https://github.com/AnswerDotAI/fastcore/issues/705)) + + ## 1.8.15 ### Bugs Squashed From af9fb7cb46f5a2fd3d3395b2d2995d49d34df965 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 8 Nov 2025 09:24:43 +1000 Subject: [PATCH 142/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 15f0ae65..bae6b8a6 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.16" +__version__ = "1.8.17" diff --git a/settings.ini b/settings.ini index 7d1053f1..076a9735 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.16 +version = 1.8.17 min_python = 3.10 audience = Developers language = English From 1ba077e96c42184d7fd9c20f3b89ea74a4900fc1 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 15 Nov 2025 04:05:30 +1000 Subject: [PATCH 143/182] spacing --- nbs/000_tour.ipynb | 2 +- nbs/00_test.ipynb | 12 ++-- nbs/01_basics.ipynb | 2 +- nbs/02_foundation.ipynb | 50 +++++++-------- nbs/03_xtras.ipynb | 132 ++++++++++++++++++++-------------------- nbs/03a_parallel.ipynb | 36 +++++------ nbs/03b_net.ipynb | 62 +++++++++---------- nbs/04_docments.ipynb | 78 ++++++++++++------------ nbs/05_meta.ipynb | 42 ++++++------- nbs/06_script.ipynb | 30 ++++----- nbs/07_xdg.ipynb | 28 ++++----- nbs/08_style.ipynb | 22 +++---- nbs/09_xml.ipynb | 8 +-- nbs/11_external.ipynb | 2 +- nbs/12_tools.ipynb | 4 +- nbs/index.ipynb | 2 +- 16 files changed, 256 insertions(+), 256 deletions(-) diff --git a/nbs/000_tour.ipynb b/nbs/000_tour.ipynb index cc2b43d3..904848a9 100644 --- a/nbs/000_tour.ipynb +++ b/nbs/000_tour.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from nbdev.showdoc import *\n", "from fastcore.all import *\n", "import numpy as np,numbers" diff --git a/nbs/00_test.ipynb b/nbs/00_test.ipynb index 95960e9c..b8f6d7ea 100644 --- a/nbs/00_test.ipynb +++ b/nbs/00_test.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|default_exp test" + "#| default_exp test" ] }, { @@ -30,7 +30,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from nbdev.showdoc import *\n", "from fastcore.nb_imports import *" ] @@ -320,7 +320,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import pandas as pd\n", "import torch" ] @@ -795,7 +795,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "def _f():\n", " with ExceptionExpected(): 1\n", "test_fail(partial(_f))\n", @@ -824,8 +824,8 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", - "#|eval: false\n", + "#| hide\n", + "#| eval: false\n", "from nbdev import nbdev_export\n", "nbdev_export()" ] diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 1f42653f..e0096a58 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|default_exp basics" + "#| default_exp basics" ] }, { diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 5e64b896..954435c9 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|default_exp foundation" + "#| default_exp foundation" ] }, { @@ -15,7 +15,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from fastcore.imports import *\n", "from fastcore.basics import *\n", "from functools import lru_cache\n", @@ -31,7 +31,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from fastcore.test import *\n", "from nbdev.showdoc import *\n", "from fastcore.nb_imports import *" @@ -59,7 +59,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@contextmanager\n", "def working_directory(path):\n", " \"Change working directory to `path` and return to previous on exit.\"\n", @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def add_docs(cls, cls_doc=None, **docs):\n", " \"Copy values from `docs` to `cls` docstrings, and confirm all public methods are documented\"\n", " if cls_doc is not None: cls.__doc__ = cls_doc\n", @@ -173,7 +173,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "class _T:\n", " def f(self): pass\n", " @classmethod\n", @@ -191,7 +191,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def docs(cls):\n", " \"Decorator version of `add_docs`, using `_docs` dict\"\n", " add_docs(cls, **cls._docs)\n", @@ -303,7 +303,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def coll_repr(c, max_n=20):\n", " \"String repr of up to `max_n` items of (possibly lazy) collection `c`\"\n", " return f'(#{len(c)}) [' + ','.join(itertools.islice(map(repr,c), max_n)) + (\n", @@ -337,7 +337,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def is_bool(x):\n", " \"Check whether `x` is a bool or None\"\n", " return isinstance(x,(bool,NoneType)) or risinstance('bool_', x)" @@ -349,7 +349,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def mask2idxs(mask):\n", " \"Convert bool mask or index list to index `L`\"\n", " if isinstance(mask,slice): return mask\n", @@ -378,7 +378,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def cycle(o):\n", " \"Like `itertools.cycle` except creates list of `None`s if `o` is empty\"\n", " o = listify(o)\n", @@ -403,7 +403,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def zip_cycle(x, *args):\n", " \"Like `itertools.zip_longest` but `cycle`s through elements of all but first argument\"\n", " return zip(x, *map(cycle,args))" @@ -424,7 +424,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def is_indexer(idx):\n", " \"Test whether `idx` will index a single item in a list\"\n", " return isinstance(idx,int) or not getattr(idx,'ndim',1)" @@ -549,7 +549,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class CollBase:\n", " \"Base class for composing a list of `items`\"\n", " def __init__(self, items): self.items = items\n", @@ -597,7 +597,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class _L_Meta(type):\n", " def __call__(cls, x=None, *args, **kwargs):\n", " if not args and not kwargs and x is not None and isinstance(x,cls): return x\n", @@ -610,7 +610,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class L(GetAttr, CollBase, metaclass=_L_Meta):\n", " \"Behaves like a list of `items` but can also index with list of indices or masks\"\n", " _default='items'\n", @@ -719,7 +719,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "add_docs(L,\n", " __getitem__=\"Retrieve `idx` (can be list of indices, or mask, or int) items\",\n", " range=\"Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`\",\n", @@ -760,8 +760,8 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", - "#|hide\n", + "#| export\n", + "#| hide\n", "# Here we are fixing the signature of L. What happens is that the __call__ method on the MetaClass of L shadows the __init__\n", "# giving the wrong signature (https://stackoverflow.com/questions/49740290/call-from-metaclass-shadows-signature-of-init).\n", "def _f(items=None, *rest, use_list=False, match=None): ...\n", @@ -774,7 +774,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "Sequence.register(L);" ] }, @@ -896,7 +896,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "# test set items with L of collections\n", "x = L([[1,2,3], [4,5], [6,7]])\n", "x[0] = [1,2]\n", @@ -2307,7 +2307,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def save_config_file(file, d, **kwargs):\n", " \"Write settings dict to a new config file, or overwrite the existing one.\"\n", " config = ConfigParser(**kwargs)\n", @@ -2321,7 +2321,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def read_config_file(file, **kwargs):\n", " config = ConfigParser(**kwargs)\n", " config.read(file, encoding='utf8')\n", @@ -2370,7 +2370,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class Config:\n", " \"Reading and writing `ConfigParser` ini files\"\n", " def __init__(self, cfg_path, cfg_name, create=None, save=True, extra_files=None, types=None, **cfg_kwargs):\n", @@ -2714,7 +2714,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] }, diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 6b3838d5..27177d8b 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|default_exp xtras" + "#| default_exp xtras" ] }, { @@ -15,7 +15,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from __future__ import annotations" ] }, @@ -25,7 +25,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from fastcore.imports import *\n", "from fastcore.foundation import *\n", "from fastcore.basics import *\n", @@ -46,7 +46,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from fastcore.test import *\n", "from nbdev.showdoc import *\n", "from fastcore.nb_imports import *\n", @@ -85,7 +85,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def walk(\n", " path:Path|str, # path to start searching\n", " symlinks:bool=True, # follow symlinks?\n", @@ -113,7 +113,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def globtastic(\n", " path:Path|str, # path to start searching\n", " recursive:bool=True, # search subfolders\n", @@ -192,7 +192,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@contextmanager\n", "def maybe_open(f, mode='r', **kwargs):\n", " \"Context manager: open `f` if it is a path (and close on exit)\"\n", @@ -365,7 +365,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def mkdir(path, exist_ok=False, parents=False, overwrite=False, **kwargs):\n", " \"Creates and returns a directory defined by `path`, optionally removing previous existing directory if `overwrite` is `True`\"\n", " import shutil\n", @@ -400,7 +400,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def image_size(fn):\n", " \"Tuple of (w,h) for png, gif, or jpg; `None` otherwise\"\n", " from fastcore import imghdr\n", @@ -441,7 +441,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def bunzip(fn):\n", " \"bunzip `fn`, raising exception if output already exists\"\n", " fn = Path(fn)\n", @@ -474,7 +474,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def loads(s, **kw):\n", " \"Same as `json.loads`, but handles `None`\"\n", " if not s: return {}\n", @@ -489,7 +489,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def loads_multi(s:str):\n", " \"Generator of >=0 decoded json dicts, possibly with non-json ignored text at start and end\"\n", " import json\n", @@ -526,7 +526,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def dumps(obj, **kw):\n", " \"Same as `json.dumps`, but uses `ujson` if available\"\n", " try: import ujson as json\n", @@ -541,7 +541,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _unpack(fname, out):\n", " import shutil\n", " shutil.unpack_archive(str(fname), str(out))\n", @@ -555,7 +555,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def untar_dir(fname, dest, rename=False, overwrite=False):\n", " \"untar `file` into `dest`, creating a directory if the root contains more than one item\"\n", " import tempfile,shutil\n", @@ -647,7 +647,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def repo_details(url):\n", " \"Tuple of `owner,name` from ssh or https git repo `url`\"\n", " res = remove_suffix(url.strip(), '.git')\n", @@ -671,7 +671,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def shell(*args, **kwargs):\n", " \"Shortcut for `subprocess.run(shell=True)`\"\n", " import subprocess\n", @@ -684,7 +684,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def ssh(host, args='', user='ubuntu', sock=None):\n", " \"Run SSH command with given arguments\"\n", " sock_opts = f'-S {sock}' if sock else ''\n", @@ -697,7 +697,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def rsync_multi(ip, files, user='ubuntu', persist='5m'):\n", " \"Transfer multiple files with rename using persistent SSH connection\"\n", " sock = f'/tmp/ssh-{ip}-{user}'\n", @@ -711,7 +711,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def run(cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, stderr=False):\n", " \"Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails\"\n", " # Even the command is same on Windows, we have to add `cmd /c `\"\n", @@ -825,7 +825,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def open_file(fn, mode='r', **kwargs):\n", " \"Open a file, with optional compression if gz or bz2 suffix\"\n", " if isinstance(fn, io.IOBase): return fn\n", @@ -843,7 +843,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def save_pickle(fn, o):\n", " \"Save a pickle file, to a file name or opened file\"\n", " import pickle\n", @@ -856,7 +856,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def load_pickle(fn):\n", " \"Load a pickle file from a file name or opened file\"\n", " import pickle\n", @@ -1009,7 +1009,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def dict2obj(d, list_func=L, dict_func=AttrDict):\n", " \"Convert (possibly nested) dicts (or lists of dicts) to `AttrDict`\"\n", " if isinstance(d, (L,list)): return list_func([dict2obj(v, list_func=list_func, dict_func=dict_func) for v in d])\n", @@ -1060,7 +1060,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def obj2dict(d):\n", " \"Convert (possibly nested) AttrDicts (or lists of AttrDicts) to `dict`\"\n", " if isinstance(d, (L,list)): return list(L(d).map(obj2dict))\n", @@ -1091,7 +1091,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _repr_dict(d, lvl):\n", " if isinstance(d,dict):\n", " its = [f\"{k}: {_repr_dict(v,lvl+1)}\" for k,v in d.items()]\n", @@ -1106,7 +1106,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def repr_dict(d):\n", " \"Print nested dicts and lists, such as returned by `dict2obj`\"\n", " return _repr_dict(d,0).strip()" @@ -1138,7 +1138,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def is_listy(x):\n", " \"`isinstance(x, (tuple,list,L,slice,Generator))`\"\n", " return isinstance(x, (tuple,list,L,slice,Generator))" @@ -1163,7 +1163,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def mapped(f, it):\n", " \"map `f` over `it`, unless it's not listy, in which case return `f(it)`\"\n", " return L(it).map(f) if is_listy(it) else f(it)" @@ -1202,7 +1202,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@patch\n", "def readlines(self:Path, hint=-1, encoding='utf8'):\n", " \"Read the content of `self`\"\n", @@ -1215,7 +1215,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@patch\n", "def read_json(self:Path, encoding=None, errors=None):\n", " \"Same as `read_text` followed by `loads`\"\n", @@ -1228,7 +1228,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@patch\n", "def mk_write(self:Path, data, encoding=None, errors=None, mode=511, uid=-1, gid=-1):\n", " \"Make all parent dirs of `self`, and write `data`\"\n", @@ -1243,7 +1243,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@patch\n", "def write_json(self:Path, data, encoding=None, errors=None, mode=511, uid=-1, gid=-1, **kw):\n", " \"Same as `dumps`followed by `mk_write`\"\n", @@ -1256,7 +1256,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@patch\n", "def relpath(self:Path, start=None):\n", " \"Same as `os.path.relpath`, but returns a `Path`, and resolves symlinks\"\n", @@ -1310,7 +1310,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@patch\n", "def ls(self:Path, n_max=None, file_type=None, file_exts=None):\n", " \"Contents of path as a list\"\n", @@ -1395,7 +1395,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "path = Path()\n", "pkl = pickle.dumps(path)\n", "p2 = pickle.loads(pkl)\n", @@ -1408,7 +1408,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@patch\n", "def __repr__(self:Path):\n", " b = getattr(Path, 'BASE_PATH', None)\n", @@ -1444,7 +1444,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@patch\n", "def delete(self:Path):\n", " \"Delete a file, symlink, or directory tree\"\n", @@ -1468,8 +1468,8 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", - "#|hide\n", + "#| export\n", + "#| hide\n", "class IterLen:\n", " \"Base class to add iteration to anything supporting `__len__` and `__getitem__`\"\n", " def __iter__(self): return (self[i] for i in range_of(self))" @@ -1481,7 +1481,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@docs\n", "class ReindexCollection(GetAttr, IterLen):\n", " \"Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`\"\n", @@ -1849,7 +1849,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "#Test ReindexCollection pickles\n", "t1 = pickle.loads(pickle.dumps(t))\n", "test_eq(list(t), list(t1))" @@ -2418,7 +2418,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _unwrapped_type_dispatch_func(x): \n", " # use isinstance_str to avoid adding plum-dispatch as dependency to fastcore\n", " return x.methods[0].implementation if isinstance_str(x,\"Function\") else x" @@ -2460,7 +2460,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _is_property(x): return type(x)==property\n", "def _has_property_getter(x): return _is_property(x) and hasattr(x, 'fget') and hasattr(x.fget, 'func')\n", "def _property_getter(x): return x.fget.func if _has_property_getter(x) else x\n", @@ -2531,7 +2531,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def truncstr(s:str, maxlen:int, suf:str='…', space='')->str:\n", " \"Truncate `s` to length `maxlen`, adding suffix `suf` if truncated\"\n", " return s[:maxlen-len(suf)]+suf if len(s)+len(space)>maxlen else s+space" @@ -2558,7 +2558,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "spark_chars = '▁▂▃▅▆▇'" ] }, @@ -2568,7 +2568,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _ceil(x, lim=None): return x if (not lim or x <= lim) else lim\n", "\n", "def _sparkchar(x, mn, mx, incr, empty_zero):\n", @@ -2584,7 +2584,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def sparkline(data, mn=None, mx=None, empty_zero=False):\n", " \"Sparkline for `data`, with `None`s (and zero, if `empty_zero`) shown as empty column\"\n", " valid = [o for o in data if o is not None]\n", @@ -2647,7 +2647,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def modify_exception(\n", " e:Exception, # An exception\n", " msg:str=None, # A custom message\n", @@ -2678,7 +2678,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def round_multiple(x, mult, round_down=False):\n", " \"Round `x` to nearest multiple of `mult`\"\n", " def _f(x_): return (int if round_down else round)(x_/mult)*mult\n", @@ -2706,7 +2706,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def set_num_threads(nt):\n", " \"Get numpy (and others) to use `nt` threads\"\n", " try: import mkl; mkl.set_num_threads(nt)\n", @@ -2734,7 +2734,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def join_path_file(file, path, ext=''):\n", " \"Return `path/file` if file is a string or a `Path`, file otherwise\"\n", " if not isinstance(file, (str, Path)): return file\n", @@ -2762,7 +2762,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def autostart(g):\n", " \"Decorator that automatically starts a generator\"\n", " @functools.wraps(g)\n", @@ -2779,7 +2779,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class EventTimer:\n", " \"An event timer with history of `store` items of time `span`\"\n", "\n", @@ -2880,7 +2880,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "_fmt = string.Formatter()" ] }, @@ -2890,7 +2890,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def stringfmt_names(s:str)->list:\n", " \"Unique brace-delimited names in `s`\"\n", " return uniqueify(o[1] for o in _fmt.parse(s) if o[1])" @@ -2912,7 +2912,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class PartialFormatter(string.Formatter):\n", " \"A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args\"\n", " def __init__(self):\n", @@ -2974,7 +2974,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def partial_format(s:str, **kwargs):\n", " \"string format `s`, ignoring missing field errors, returning missing and extra fields\"\n", " fmt = PartialFormatter()\n", @@ -3007,7 +3007,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def utc2local(dt:datetime)->datetime:\n", " \"Convert `dt` from UTC to local time\"\n", " return dt.replace(tzinfo=timezone.utc).astimezone(tz=None)" @@ -3037,7 +3037,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def local2utc(dt:datetime)->datetime:\n", " \"Convert `dt` from local to UTC time\"\n", " return dt.replace(tzinfo=None).astimezone(tz=timezone.utc)" @@ -3066,7 +3066,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def trace(f):\n", " \"Add `set_trace` to an existing function `f`\"\n", " from pdb import set_trace\n", @@ -3098,7 +3098,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@contextmanager\n", "def modified_env(*delete, **replace):\n", " \"Context manager temporarily modifying `os.environ` by deleting `delete` and replacing `replace`\"\n", @@ -3137,7 +3137,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class ContextManagers(GetAttr):\n", " \"Wrapper for `contextlib.ExitStack` which enters a collection of context managers\"\n", " def __init__(self, mgrs): self.default,self.stack = L(mgrs),ExitStack()\n", @@ -3190,7 +3190,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def shufflish(x, pct=0.04):\n", " \"Randomly relocate items of `x` up to `pct` of `len(x)` from their starting location\"\n", " n = len(x)\n", @@ -3204,7 +3204,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def console_help(\n", " libname:str): # name of library for console script listing\n", " \"Show help for all console scripts from `libname`\"\n", @@ -4095,7 +4095,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] }, diff --git a/nbs/03a_parallel.ipynb b/nbs/03a_parallel.ipynb index c33251b8..285e528f 100644 --- a/nbs/03a_parallel.ipynb +++ b/nbs/03a_parallel.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|default_exp parallel" + "#| default_exp parallel" ] }, { @@ -15,7 +15,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from fastcore.imports import *\n", "from fastcore.basics import *\n", "from fastcore.foundation import *\n", @@ -57,7 +57,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def threaded(process=False):\n", " \"Run `f` in a `Thread` (or `Process` if `process=True`), and returns it\"\n", " def _r(f):\n", @@ -143,7 +143,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def startthread(f):\n", " \"Like `threaded`, but start thread immediately\"\n", " return threaded(f)()" @@ -183,7 +183,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def startproc(f):\n", " \"Like `threaded(True)`, but start Process immediately\"\n", " return threaded(True)(f)()" @@ -223,7 +223,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _call(lock, pause, n, g, item):\n", " l = False\n", " if pause:\n", @@ -241,7 +241,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def parallelable(param_name, num_workers, f=None):\n", " f_in_main = f == None or sys.modules[f.__module__].__name__ == \"__main__\"\n", " if sys.platform == \"win32\" and IN_NOTEBOOK and num_workers > 0 and f_in_main:\n", @@ -257,7 +257,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class ThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):\n", " \"Same as Python's ThreadPoolExecutor, except can pass `max_workers==0` for serial execution\"\n", " def __init__(self, max_workers=defaults.cpus, on_exc=print, pause=0, **kwargs):\n", @@ -323,7 +323,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@delegates()\n", "class ProcessPoolExecutor(concurrent.futures.ProcessPoolExecutor):\n", " \"Same as Python's ProcessPoolExecutor, except can pass `max_workers==0` for serial execution\"\n", @@ -396,7 +396,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "try: from fastprogress import progress_bar\n", "except: progress_bar = None" ] @@ -407,7 +407,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def parallel(f, items, *args, n_workers=defaults.cpus, total=None, progress=None, pause=0,\n", " method=None, threadpool=False, timeout=None, chunksize=1, **kwargs):\n", " \"Applies `func` in parallel to `items`, using `n_workers`\"\n", @@ -431,7 +431,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _add_one(x, a=1):\n", " # this import is necessary for multiprocessing in notebook on windows\n", " import random\n", @@ -512,7 +512,7 @@ } ], "source": [ - "#|hide\n", + "#| hide\n", "def die_sometimes(x):\n", "# if 3dict:\n", " \"Summary containing full_url, headers, method, and data, removing `skip` from headers\"\n", @@ -620,7 +620,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def urlsend(url, verb, headers=None, decode=True, route=None, query=None, data=None, json_data=True,\n", " return_json=True, return_headers=False, debug=None, timeout=None):\n", " \"Send request with `urlrequest`, converting result to json if `return_json`\"\n", @@ -639,7 +639,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def do_request(url, post=False, headers=None, **data):\n", " \"Call GET or json-encoded POST on `url`, depending on `post`\"\n", " if data:\n", @@ -663,7 +663,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _socket_det(port,host,dgram):\n", " if isinstance(port,int): family,addr = socket.AF_INET,(host or socket.gethostname(),port)\n", " else: family,addr = socket.AF_UNIX,port\n", @@ -676,7 +676,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def start_server(port, host=None, dgram=False, reuse_addr=True, n_queue=None):\n", " \"Create a `socket` server on `port`, with optional `host`, of type `dgram`\"\n", " listen_args = [n_queue] if n_queue else []\n", @@ -704,7 +704,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def start_client(port, host=None, dgram=False):\n", " \"Create a `socket` client on `port`, with optional `host`, of type `dgram`\"\n", " family,addr,typ = _socket_det(port,host,dgram)\n", @@ -719,7 +719,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def tobytes(s:str)->bytes:\n", " \"Convert `s` into HTTP-ready bytes format\"\n", " return s.replace('\\n', '\\r\\n').encode('utf-8')" @@ -740,7 +740,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def http_response(body=None, status=200, hdrs=None, **kwargs):\n", " \"Create an HTTP-ready response, adding `kwargs` to `hdrs`\"\n", " kwargs = {k.replace('_','-'):v for k,v in kwargs.items()}\n", @@ -768,7 +768,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@threaded\n", "def recv_once(host:str='localhost', port:int=8000):\n", " \"Spawn a thread to receive a single HTTP request and store in `d['r']`\"\n", @@ -791,7 +791,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from nbdev import nbdev_export\n", "nbdev_export()" ] diff --git a/nbs/04_docments.ipynb b/nbs/04_docments.ipynb index c597ae87..d8011721 100644 --- a/nbs/04_docments.ipynb +++ b/nbs/04_docments.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|default_exp docments" + "#| default_exp docments" ] }, { @@ -24,7 +24,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from __future__ import annotations\n", "\n", "import re,ast,inspect\n", @@ -48,7 +48,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from nbdev.showdoc import *\n", "from fastcore.test import *" ] @@ -142,7 +142,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def docstring(sym):\n", " \"Get docstring for `sym` for functions ad classes\"\n", " if isinstance(sym, str): return sym\n", @@ -166,7 +166,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def parse_docstring(sym):\n", " \"Parse a numpy-style docstring in `sym`\"\n", " return AttrDict(**docscrape.NumpyDocString(docstring(sym)))" @@ -187,7 +187,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def isdataclass(s):\n", " \"Check if `s` is a dataclass but not a dataclass' instance\"\n", " return is_dataclass(s) and isclass(s)" @@ -199,7 +199,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def get_dataclass_source(s):\n", " \"Get source code for dataclass `s`\"\n", " return getsource(s) if not getattr(s, \"__module__\") == '__main__' else \"\"" @@ -211,7 +211,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def get_source(s):\n", " \"Get source code for string, function object or dataclass `s`\"\n", " if isinstance(s,str): return s\n", @@ -224,7 +224,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _parses(s):\n", " \"Parse Python code in string, function object or dataclass `s`\"\n", " return parse(dedent(get_source(s) or ''))\n", @@ -288,7 +288,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "empty = Parameter.empty" ] }, @@ -298,7 +298,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _get_comment(line, arg, comments, parms):\n", " if line in comments: return comments[line].strip()\n", " line -= 1\n", @@ -342,7 +342,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _merge_doc(dm, npdoc):\n", " if not npdoc: return dm\n", " if not dm.anno or dm.anno==empty: dm.anno = npdoc.type\n", @@ -362,7 +362,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _get_property_name(p):\n", " \"Get the name of property `p`\"\n", " if hasattr(p, 'fget'):\n", @@ -376,7 +376,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def get_name(obj):\n", " \"Get the name of `obj`\"\n", " if hasattr(obj, '__name__'): return obj.__name__\n", @@ -402,7 +402,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def qual_name(obj):\n", " \"Get the qualified name of `obj`\"\n", " if hasattr(obj,'__qualname__'): return obj.__qualname__\n", @@ -432,7 +432,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _docments(s, returns=True, eval_str=False, args_kwargs=False):\n", " \"`dict` of parameter names to 'docment-style' comments in function or string `s`\"\n", " nps = parse_docstring(s)\n", @@ -458,7 +458,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "@delegates(_docments)\n", "def docments(elt, full=False, args_kwargs=False, **kwargs):\n", " \"Generates a `docment`\"\n", @@ -968,7 +968,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "class _F:\n", " @classmethod\n", " def class_method(cls, \n", @@ -1068,7 +1068,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "def _c(b:str, # Second\n", " a:int=2): return b, a # Third\n", "\n", @@ -1085,7 +1085,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "test_eq(docments(_c, full=True)['b']['docment'],'Second')\n", "test_eq(docments(_d, full=True)['b']['docment'],'Second')\n", "_argset = {'a', 'b', 'c', 'return'}\n", @@ -1278,7 +1278,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _non_empty_keys(d:dict): return L([k for k,v in d.items() if v != inspect._empty])\n", "def _bold(s): return f'**{s}**' if s.strip() else s" ] @@ -1289,7 +1289,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _escape_markdown(s):\n", " for c in '|^': s = re.sub(rf'\\\\?\\{c}', rf'\\{c}', s)\n", " return s.replace('\\n', '
')" @@ -1301,7 +1301,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "test_eq(_escape_markdown('|'), '\\|')\n", "test_eq(_escape_markdown('\\|'), '\\|')\n", "test_eq(_escape_markdown(' ^[_'), ' \\^[_') # footnotes\n", @@ -1316,7 +1316,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _maybe_nm(o):\n", " if (o == inspect._empty): return ''\n", " else: return o.__name__ if hasattr(o, '__name__') else _escape_markdown(str(o))" @@ -1328,7 +1328,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "test_eq(_maybe_nm(list), 'list')\n", "test_eq(_maybe_nm('fastai'), 'fastai')" ] @@ -1339,7 +1339,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _list2row(l:list): return '| '+' | '.join([_maybe_nm(o) for o in l]) + ' |'" ] }, @@ -1349,7 +1349,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "test_eq(_list2row(['Hamel', 'Jeremy']), '| Hamel | Jeremy |')\n", "test_eq(_list2row([inspect._empty, bool, 'foo']), '| | bool | foo |')" ] @@ -1360,7 +1360,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class DocmentTbl:\n", " # this is the column order we want these items to appear\n", " _map = {'anno':'Type', 'default':'Default', 'docment':'Details'}\n", @@ -1478,7 +1478,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "_exp_res=\"\"\"\n", "| | **Type** | **Default** | **Details** |\n", "| -- | -------- | ----------- | ----------- |\n", @@ -1537,7 +1537,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "_exp_res2 = \"\"\"\n", "| | **Details** |\n", "| -- | ----------- |\n", @@ -1642,7 +1642,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "_exp_res3 = \"\"\"\n", "| | **Type** | **Default** | **Details** |\n", "| -- | -------- | ----------- | ----------- |\n", @@ -1673,7 +1673,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _docstring(sym):\n", " npdoc = parse_docstring(sym)\n", " return '\\n\\n'.join([npdoc['Summary'], npdoc['Extended']]).strip()" @@ -1685,7 +1685,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _fullname(o):\n", " module,name = getattr(o, \"__module__\", None),qual_name(o)\n", " return name if module is None or module in ('__main__','builtins') else module + '.' + name\n", @@ -1713,7 +1713,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _f_name(o): return f'' if isinstance(o, FunctionType) else None\n", "def _fmt_anno(o): return inspect.formatannotation(o).strip(\"'\").replace(' ','')\n", "\n", @@ -1733,7 +1733,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "def _func(): pass\n", "p = Parameter('foo', Parameter.POSITIONAL_OR_KEYWORD, default=_func, annotation='Callable')\n", "test_eq(_show_param(p), 'foo:Callable=')\n", @@ -1747,7 +1747,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _fmt_sig(sig):\n", " if sig is None: return ''\n", " p = {k:v for k,v in sig.parameters.items()}\n", @@ -1771,7 +1771,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "def _long_f(a_param, b_param=True, c_param:str='Some quite long value', d:int=2, e:bool=False):\n", " \"A docstring\"\n", " ...\n", @@ -1787,7 +1787,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _ext_link(url, txt, xtra=\"\"): return f'[{txt}]({url}){{target=\"_blank\" {xtra}}}'\n", "\n", "class MarkdownRenderer(ShowDocRenderer):\n", @@ -1861,7 +1861,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] }, diff --git a/nbs/05_meta.ipynb b/nbs/05_meta.ipynb index b93a5288..eaebf422 100644 --- a/nbs/05_meta.ipynb +++ b/nbs/05_meta.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|default_exp meta" + "#| default_exp meta" ] }, { @@ -15,7 +15,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from fastcore.imports import *\n", "from fastcore.test import *\n", "from contextlib import contextmanager\n", @@ -61,7 +61,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def test_sig(f, b):\n", " \"Test the signature of an object\"\n", " test_eq(str(inspect.signature(f)), b)" @@ -90,7 +90,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export \n", + "#| export \n", "def _rm_self(sig):\n", " sigd = dict(sig.parameters)\n", " sigd.pop('self')\n", @@ -103,7 +103,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class FixSigMeta(type):\n", " \"A metaclass that fixes the signature on classes that override `__new__`\"\n", " def __new__(cls, name, bases, dict):\n", @@ -325,7 +325,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class PrePostInitMeta(FixSigMeta):\n", " \"A metaclass that calls optional `__pre_init__` and `__post_init__` methods\"\n", " def __call__(cls, *args, **kwargs):\n", @@ -413,7 +413,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class AutoInit(metaclass=PrePostInitMeta):\n", " \"Same as `object`, but no need for subclasses to call `super().__init__`\"\n", " def __pre_init__(self, *args, **kwargs): super().__init__(*args, **kwargs)" @@ -449,7 +449,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class NewChkMeta(FixSigMeta):\n", " \"Metaclass to avoid recreating object passed to constructor\"\n", " def __call__(cls, x=None, *args, **kwargs):\n", @@ -621,7 +621,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class BypassNewMeta(FixSigMeta):\n", " \"Metaclass: casts `x` to this class if it's of type `cls._bypass_type`\"\n", " def __call__(cls, x=None, *args, **kwargs):\n", @@ -756,7 +756,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def empty2none(p):\n", " \"Replace `Parameter.empty` with `None`\"\n", " return None if p==inspect.Parameter.empty else p" @@ -768,7 +768,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def anno_dict(f):\n", " \"`__annotation__ dictionary with `empty` cast to `None`, returning empty if doesn't exist\"\n", " return {k:empty2none(v) for k,v in getattr(f, '__annotations__', {}).items()}" @@ -790,7 +790,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _mk_param(n,d=None): return inspect.Parameter(n, inspect.Parameter.KEYWORD_ONLY, default=d)" ] }, @@ -800,7 +800,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def use_kwargs_dict(keep=False, **kwargs):\n", " \"Decorator: replace `**kwargs` in signature with `names` params\"\n", " def _f(f):\n", @@ -859,7 +859,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def use_kwargs(names, keep=False):\n", " \"Decorator: replace `**kwargs` in signature with `names` params\"\n", " def _f(f):\n", @@ -917,7 +917,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def delegates(to:FunctionType=None, # Delegatee\n", " keep=False, # Keep `kwargs` in decorated function?\n", " but:list=None, # Exclude these parameters from signature\n", @@ -1207,7 +1207,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def method(f):\n", " \"Mark `f` as a method\"\n", " # `1` is a dummy instance since Py3 doesn't allow `None` any more\n", @@ -1298,7 +1298,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _funcs_kwargs(cls, as_method):\n", " old_init = cls.__init__\n", " def _init(self, *args, **kwargs):\n", @@ -1321,7 +1321,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def funcs_kwargs(as_method=False):\n", " \"Replace methods in `cls._methods` with those from `kwargs`\"\n", " if callable(as_method): return _funcs_kwargs(as_method, False)\n", @@ -1498,7 +1498,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "def _g(a=1): return a+1\n", "class T3(T): b = staticmethod(_g)\n", "t = T3()\n", @@ -1511,7 +1511,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "#test funcs_kwargs works with PrePostInitMeta\n", "class A(metaclass=PrePostInitMeta): pass\n", "\n", @@ -1536,7 +1536,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] }, diff --git a/nbs/06_script.ipynb b/nbs/06_script.ipynb index 54737b1b..75d6b0b5 100644 --- a/nbs/06_script.ipynb +++ b/nbs/06_script.ipynb @@ -6,8 +6,8 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", - "#|default_exp script" + "#| hide\n", + "#| default_exp script" ] }, { @@ -162,7 +162,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "import inspect,argparse,shutil\n", "from functools import wraps,partial\n", "from fastcore.imports import *\n", @@ -176,7 +176,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from fastcore.test import *" ] }, @@ -186,7 +186,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def store_true():\n", " \"Placeholder to pass to `Param` for `store_true` action\"\n", " pass" @@ -198,7 +198,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def store_false():\n", " \"Placeholder to pass to `Param` for `store_false` action\"\n", " pass" @@ -210,7 +210,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def bool_arg(v):\n", " \"Use as `type` for `Param` to get `bool` behavior\"\n", " return str2bool(v)" @@ -222,7 +222,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def clean_type_str(x:str):\n", " x = str(x)\n", " x = re.sub(r\"(enum |class|function|__main__\\.|\\ at.*)\", '', x)\n", @@ -258,7 +258,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class Param:\n", " \"A parameter in a function used in `anno_parser` or `call_parse`\"\n", " def __init__(self, help=\"\", type=None, opt=True, action=None, nargs=None, const=None,\n", @@ -373,7 +373,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class _HelpFormatter(argparse.HelpFormatter):\n", " def __init__(self, prog, indent_increment=2):\n", " cols = shutil.get_terminal_size((120,30))[0]\n", @@ -387,7 +387,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def anno_parser(func, # Function to get arguments from\n", " prog:str=None): # The name of the program\n", " \"Look at params (annotated with `Param`) in func and return an `ArgumentParser`\"\n", @@ -552,7 +552,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def args_from_prog(func, prog):\n", " \"Extract args from `prog`\"\n", " if prog is None or '#' not in prog: return {}\n", @@ -590,7 +590,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "SCRIPT_INFO = SimpleNamespace(func=None)" ] }, @@ -607,7 +607,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def call_parse(func=None, nested=False):\n", " \"Decorator to create a simple CLI from `func` using `anno_parser`\"\n", " if func is None: return partial(call_parse, nested=nested)\n", @@ -704,7 +704,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] }, diff --git a/nbs/07_xdg.ipynb b/nbs/07_xdg.ipynb index 289e90c9..0e7cc06e 100644 --- a/nbs/07_xdg.ipynb +++ b/nbs/07_xdg.ipynb @@ -7,8 +7,8 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", - "#|default_exp xdg" + "#| hide\n", + "#| default_exp xdg" ] }, { @@ -36,7 +36,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "from fastcore.utils import *" ] }, @@ -122,7 +122,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _path_from_env(variable, default):\n", " value = os.environ.get(variable)\n", " if value and os.path.isabs(value): return Path(value)\n", @@ -136,7 +136,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _paths_from_env(variable, default):\n", " value = os.environ.get(variable)\n", " if value:\n", @@ -152,7 +152,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def xdg_cache_home():\n", " \"Path corresponding to `XDG_CACHE_HOME`\"\n", " return _path_from_env(\"XDG_CACHE_HOME\", Path.home()/\".cache\")" @@ -187,7 +187,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def xdg_config_dirs():\n", " \"Paths corresponding to `XDG_CONFIG_DIRS`\"\n", " return _paths_from_env(\"XDG_CONFIG_DIRS\", [Path(\"/etc/xdg\")])" @@ -212,7 +212,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def xdg_config_home():\n", " \"Path corresponding to `XDG_CONFIG_HOME`\"\n", " return _path_from_env(\"XDG_CONFIG_HOME\", Path.home()/\".config\")" @@ -237,7 +237,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def xdg_data_dirs():\n", " \"Paths corresponding to XDG_DATA_DIRS`\"\n", " return _paths_from_env( \"XDG_DATA_DIRS\", [Path(o) for o in \"/usr/local/share/:/usr/share/\".split(\":\")])" @@ -250,7 +250,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def xdg_data_home():\n", " \"Path corresponding to `XDG_DATA_HOME`\"\n", " return _path_from_env(\"XDG_DATA_HOME\", Path.home()/\".local\"/\"share\")" @@ -275,7 +275,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def xdg_runtime_dir():\n", " \"Path corresponding to `XDG_RUNTIME_DIR`\"\n", " value = os.getenv(\"XDG_RUNTIME_DIR\")\n", @@ -289,7 +289,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def xdg_state_home():\n", " \"Path corresponding to `XDG_STATE_HOME`\"\n", " return _path_from_env(\"XDG_STATE_HOME\", Path.home()/\".local\"/\"state\")" @@ -322,8 +322,8 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", - "#|eval: false\n", + "#| hide\n", + "#| eval: false\n", "from nbdev import nbdev_export\n", "nbdev_export()" ] diff --git a/nbs/08_style.ipynb b/nbs/08_style.ipynb index a2f7a56e..75bc17a7 100644 --- a/nbs/08_style.ipynb +++ b/nbs/08_style.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|default_exp style" + "#| default_exp style" ] }, { @@ -39,7 +39,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| 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", @@ -54,7 +54,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| 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", @@ -94,7 +94,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| 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", @@ -109,7 +109,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| 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", @@ -128,7 +128,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| 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", @@ -142,7 +142,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| 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", @@ -176,7 +176,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|exports\n", + "#| exports\n", "S = Style()" ] }, @@ -353,7 +353,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _demo(name, code):\n", " s = getattr(S,name)\n", " print(s(f'{code.code:>3} {name:16}'))" @@ -366,7 +366,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def demo():\n", " \"Demonstrate all available styles and their codes.\"\n", " for k,v in style_codes.items(): _demo(k,v)" @@ -455,7 +455,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] }, diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb index 854d048c..bd16c3f1 100644 --- a/nbs/09_xml.ipynb +++ b/nbs/09_xml.ipynb @@ -66,7 +66,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _fix_k(k): return k if k=='_' else k.lstrip('_').replace('_', '-')" ] }, @@ -125,7 +125,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "def _preproc(c, kw, attrmap=attrmap, valmap=valmap):\n", " if len(c)==1 and isinstance(c[0], (types.GeneratorType, map, filter)): c = tuple(c[0])\n", " attrs = {attrmap(k.lower()):valmap(v) for k,v in kw.items() if v is not None}\n", @@ -139,7 +139,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|export\n", + "#| export\n", "class FT:\n", " \"A 'Fast Tag' structure, containing `tag`,`children`,and `attrs`\"\n", " def __init__(self, tag:str, cs:tuple, attrs:dict=None, void_=False, **kwargs):\n", @@ -897,7 +897,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "import nbdev; nbdev.nbdev_export()" ] }, diff --git a/nbs/11_external.ipynb b/nbs/11_external.ipynb index 42d0c0d3..324cacdc 100644 --- a/nbs/11_external.ipynb +++ b/nbs/11_external.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from nbdev.showdoc import *\n", "from fastcore.utils import *\n", "from IPython.display import HTML" diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index c169b587..0f66cdfa 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -729,8 +729,8 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", - "#|eval: false\n", + "#| hide\n", + "#| eval: false\n", "from nbdev import nbdev_export\n", "nbdev_export()" ] diff --git a/nbs/index.ipynb b/nbs/index.ipynb index 4166ec75..f33c5fec 100644 --- a/nbs/index.ipynb +++ b/nbs/index.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "outputs": [], "source": [ - "#|hide\n", + "#| hide\n", "from nbdev.showdoc import *\n", "from fastcore.all import *\n", "import numpy as np,numbers" From 50246ba92e3a2c851a775d831333d7d9d624d3a7 Mon Sep 17 00:00:00 2001 From: s01st Date: Fri, 21 Nov 2025 09:14:08 -0600 Subject: [PATCH 144/182] patched NumpyDocString class to support parameter formated sections besides just Parameters e.g. Attributes. --- fastcore/docscrape.py | 50 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/fastcore/docscrape.py b/fastcore/docscrape.py index 74e854ee..b0c3b550 100644 --- a/fastcore/docscrape.py +++ b/fastcore/docscrape.py @@ -25,6 +25,7 @@ from warnings import warn from collections import namedtuple from collections.abc import Mapping +from typing import Any __all__ = ['Parameter', 'NumpyDocString', 'dedent_lines'] @@ -93,6 +94,20 @@ def __str__(self): SECTIONS = 'Summary Extended Yields Receives Other Raises Warns Warnings See Also Notes References Examples Attributes Methods'.split() +'''Named `numpydoc` sections see: https://numpydoc.readthedocs.io/en/latest/format.html#sections''' + +PARAM_SECTIONS = { + "Parameters", + "Other Parameters", + "Attributes", + "Methods", + "Raises", + "Warns", + "Yields", + "Receives" +} +'''Set of `numpydoc` sections which should support parameters via `Parameter`.''' + class NumpyDocString(Mapping): "Parses a numpydoc string to an abstract representation" @@ -102,14 +117,26 @@ class NumpyDocString(Mapping): sections['Parameters'] = [] sections['Returns'] = [] - def __init__(self, docstring, config=None): + def __init__(self, docstring, config=None, supports_params: set[str] = PARAM_SECTIONS): + # --- original initialization --- docstring = textwrap.dedent(docstring).split('\n') self._doc = Reader(docstring) self._parsed_data = copy.deepcopy(self.sections) self._parse() + + # --- fastcore default normalization --- self['Parameters'] = {o.name:o for o in self['Parameters']} if self['Returns']: self['Returns'] = self['Returns'][0] - for section in SECTIONS: self[section] = dedent_lines(self[section], split=False) + + # --- our patch: normalize ALL parameter-like sections --- + for sec in supports_params: + if sec in self._parsed_data: + self._parsed_data[sec] = self._normalize_param_section(self._parsed_data[sec]) + + + # --- continue normal fastcore behavior --- + for section in SECTIONS: + self[section] = dedent_lines(self[section], split=False) def __iter__(self): return iter(self._parsed_data) def __len__(self): return len(self._parsed_data) @@ -174,6 +201,25 @@ def _parse_param_list(self, content, single_element_is_type=False): params.append(Parameter(arg_name, arg_type, desc)) return params + def _normalize_param_section(self, val: list[Parameter] | Any) -> dict[Parameter] | Any: + """ + Convert lists of `Parameter` objects into a dict or clean list. + """ + # Not a list? Then noop. + if not isinstance(val, list): + return val + + # Falsy value i.e. empty list? Then noop. + if not val: + return val + + # Lazy check, assumes if first value is a Parameter, all are. + if not isinstance(val[0], Parameter): + return val + + # Convert to dict[name -> Parameter] + return {p.name: p for p in val} + def _parse_summary(self): """Grab signature (if given) and summary""" if self._is_at_section(): return From ea9bbc7d047f9bbde37610bc797188919e408d05 Mon Sep 17 00:00:00 2001 From: s01st Date: Fri, 21 Nov 2025 09:29:30 -0600 Subject: [PATCH 145/182] added some configuration for which sections support parameters and exposed them via __init__ --- fastcore/docscrape.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/fastcore/docscrape.py b/fastcore/docscrape.py index b0c3b550..27f52a8f 100644 --- a/fastcore/docscrape.py +++ b/fastcore/docscrape.py @@ -112,12 +112,41 @@ def __str__(self): class NumpyDocString(Mapping): "Parses a numpydoc string to an abstract representation" # See the NumPy Doc Manual https://numpydoc.readthedocs.io/en/latest/format.html> + # TODO: flushout docstring + + # NOTE: unclear why these are class variables sections = {o:[] for o in SECTIONS} sections['Summary'] = [''] + + # NOTE: unclear why these are not included in `SECTIONS` given initialization above creates lists sections['Parameters'] = [] sections['Returns'] = [] - def __init__(self, docstring, config=None, supports_params: set[str] = PARAM_SECTIONS): + # NOTE: following above style, adding `param_sections` as class variable + param_sections: set[str] = set(PARAM_SECTIONS) + + def __init__( + self, docstring, + config=None, # TODO: figure this out + supported_sections: list[str] | None = SECTIONS, + supports_params: set[str] | None = PARAM_SECTIONS + ): + + # If None, set to default supported set + if supports_params is None: supports_params = set(PARAM_SECTIONS) + else: + # add missing to class variable + missing = set(supports_params) - set(self.param_sections) + for sec in missing: self.param_sections.add(sec) + + # If None, set to default supported set + if supported_sections is None: supported_sections = set(SECTIONS) + else: + # add missing to class variable + missing = set(supported_sections) - set(self.sections.keys()) + for sec in missing: self.sections[sec] = [] + + # --- original initialization --- docstring = textwrap.dedent(docstring).split('\n') self._doc = Reader(docstring) From 3a374d6d4ea884279470fd4d58367baabc54fc82 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 24 Nov 2025 16:51:52 +1000 Subject: [PATCH 146/182] fixes #707 --- fastcore/_modidx.py | 1 + fastcore/xtras.py | 28 +++++++++++------- nbs/03_xtras.ipynb | 70 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 56f6b50b..c8f08b97 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -669,6 +669,7 @@ 'fastcore.xtras.globtastic': ('xtras.html#globtastic', 'fastcore/xtras.py'), 'fastcore.xtras.hl_md': ('xtras.html#hl_md', 'fastcore/xtras.py'), 'fastcore.xtras.image_size': ('xtras.html#image_size', 'fastcore/xtras.py'), + 'fastcore.xtras.img_bytes': ('xtras.html#img_bytes', 'fastcore/xtras.py'), 'fastcore.xtras.is_listy': ('xtras.html#is_listy', 'fastcore/xtras.py'), 'fastcore.xtras.is_namedtuple': ('xtras.html#is_namedtuple', 'fastcore/xtras.py'), 'fastcore.xtras.is_typeddict': ('xtras.html#is_typeddict', 'fastcore/xtras.py'), diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 06813b05..8dcb5697 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -6,16 +6,16 @@ from __future__ import annotations # %% auto 0 -__all__ = ['spark_chars', 'UNSET', 'walk', 'globtastic', 'maybe_open', 'mkdir', 'image_size', 'bunzip', 'loads', 'loads_multi', - 'dumps', 'untar_dir', 'repo_details', 'shell', 'ssh', 'rsync_multi', 'run', 'open_file', 'save_pickle', - 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'mapped', - 'IterLen', 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', 'friendly_name', - 'n_friendly_names', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', 'modify_exception', - 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', - 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', 'ContextManagers', - 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', 'make_nullable', - 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict', 'is_namedtuple', 'CachedIter', 'CachedAwaitable', - 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] +__all__ = ['spark_chars', 'UNSET', 'walk', 'globtastic', 'maybe_open', 'mkdir', 'image_size', 'img_bytes', 'bunzip', 'loads', + 'loads_multi', 'dumps', 'untar_dir', 'repo_details', 'shell', 'ssh', 'rsync_multi', 'run', 'open_file', + 'save_pickle', 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', + 'is_listy', 'mapped', 'IterLen', 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', + 'friendly_name', 'n_friendly_names', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', + 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', + 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', + 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', + 'make_nullable', 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict', 'is_namedtuple', 'CachedIter', + 'CachedAwaitable', 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] # %% ../nbs/03_xtras.ipynb from .imports import * @@ -126,6 +126,14 @@ def _png_size(f): d = dict(png=_png_size, gif=_gif_size, jpeg=_jpg_size) with maybe_open(fn, 'rb') as f: return d[imghdr.what(f)](f) +# %% ../nbs/03_xtras.ipynb +def img_bytes(img, fmt='PNG'): + # Convert PIL `img` to bytes in format `fmt` + from io import BytesIO + buf=BytesIO() + img.save(buf, format=fmt) + return buf.getvalue() + # %% ../nbs/03_xtras.ipynb def bunzip(fn): "bunzip `fn`, raising exception if output already exists" diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 27177d8b..6ef38b76 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -435,6 +435,76 @@ "test_eq(image_size(fname), (1200,803))" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from PIL import Image\n", + "from IPython.display import Image as IPImage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAyADIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDi6KKK+ZP3EKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAfUlEQVR4AWL8zzAYAdNgdBTDqLNIAqORSAoYDS1SwGhokQJGQ4sUMBpapIDR0CIFjIYWKWA0tEgBo6FFChgNLVLAaGiRAkZDixQwGlqkgNHQIgWMhhYpYDS0SAGjoUUKGA0tUsBoaJECRkOLFDAaWqSA0dAiBYyGFmCkhBYAQaoBY/NmMJ4AAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "img = Image.new('RGB', (50, 50), color='red')\n", + "img" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def img_bytes(img, fmt='PNG'):\n", + " # Convert PIL `img` to bytes in format `fmt`\n", + " from io import BytesIO\n", + " buf=BytesIO()\n", + " img.save(buf, format=fmt)\n", + " return buf.getvalue()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAZklEQVR4nM3OMQEAMAyAMIZ/z52B/iUK8oYiSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkrwO7D0GqAWPcq78HAAAAAElFTkSuQmCC", + "text/plain": [ + "" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ib = img_bytes(img)\n", + "IPImage(ib)" + ] + }, { "cell_type": "code", "execution_count": null, From 772dcd4b6e14108d42811a323968c2651300bcc5 Mon Sep 17 00:00:00 2001 From: s01st Date: Mon, 24 Nov 2025 08:32:26 -0600 Subject: [PATCH 147/182] removed todos and commnets per request --- fastcore/docscrape.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/fastcore/docscrape.py b/fastcore/docscrape.py index 27f52a8f..5b81a6f6 100644 --- a/fastcore/docscrape.py +++ b/fastcore/docscrape.py @@ -94,48 +94,31 @@ def __str__(self): SECTIONS = 'Summary Extended Yields Receives Other Raises Warns Warnings See Also Notes References Examples Attributes Methods'.split() -'''Named `numpydoc` sections see: https://numpydoc.readthedocs.io/en/latest/format.html#sections''' PARAM_SECTIONS = { - "Parameters", - "Other Parameters", - "Attributes", - "Methods", - "Raises", - "Warns", - "Yields", - "Receives" + "Parameters", "Other Parameters", "Attributes", "Methods", + "Raises", "Warns", "Yields", "Receives" } -'''Set of `numpydoc` sections which should support parameters via `Parameter`.''' - class NumpyDocString(Mapping): "Parses a numpydoc string to an abstract representation" # See the NumPy Doc Manual https://numpydoc.readthedocs.io/en/latest/format.html> - # TODO: flushout docstring - # NOTE: unclear why these are class variables sections = {o:[] for o in SECTIONS} sections['Summary'] = [''] - - # NOTE: unclear why these are not included in `SECTIONS` given initialization above creates lists sections['Parameters'] = [] sections['Returns'] = [] - - # NOTE: following above style, adding `param_sections` as class variable param_sections: set[str] = set(PARAM_SECTIONS) def __init__( self, docstring, - config=None, # TODO: figure this out + config=None, supported_sections: list[str] | None = SECTIONS, supports_params: set[str] | None = PARAM_SECTIONS ): - # If None, set to default supported set if supports_params is None: supports_params = set(PARAM_SECTIONS) else: - # add missing to class variable missing = set(supports_params) - set(self.param_sections) for sec in missing: self.param_sections.add(sec) @@ -157,7 +140,7 @@ def __init__( self['Parameters'] = {o.name:o for o in self['Parameters']} if self['Returns']: self['Returns'] = self['Returns'][0] - # --- our patch: normalize ALL parameter-like sections --- + # --- normalize ALL parameter-like sections --- for sec in supports_params: if sec in self._parsed_data: self._parsed_data[sec] = self._normalize_param_section(self._parsed_data[sec]) From 9cc2c120eac8b48bf880fe1e65bbe4bee9d38f74 Mon Sep 17 00:00:00 2001 From: s01st Date: Tue, 25 Nov 2025 08:23:31 -0600 Subject: [PATCH 148/182] reduced number of lines to adhere to prefered style --- fastcore/docscrape.py | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/fastcore/docscrape.py b/fastcore/docscrape.py index 5b81a6f6..4b29fe3f 100644 --- a/fastcore/docscrape.py +++ b/fastcore/docscrape.py @@ -94,11 +94,7 @@ def __str__(self): SECTIONS = 'Summary Extended Yields Receives Other Raises Warns Warnings See Also Notes References Examples Attributes Methods'.split() - -PARAM_SECTIONS = { - "Parameters", "Other Parameters", "Attributes", "Methods", - "Raises", "Warns", "Yields", "Receives" -} +PARAM_SECTIONS = {"Parameters", "Other Parameters", "Attributes", "Methods", "Raises", "Warns", "Yields", "Receives"} class NumpyDocString(Mapping): "Parses a numpydoc string to an abstract representation" @@ -110,12 +106,7 @@ class NumpyDocString(Mapping): sections['Returns'] = [] param_sections: set[str] = set(PARAM_SECTIONS) - def __init__( - self, docstring, - config=None, - supported_sections: list[str] | None = SECTIONS, - supports_params: set[str] | None = PARAM_SECTIONS - ): + def __init__(self, docstring, config=None, supported_sections: list[str] | None = SECTIONS, supports_params: set[str] | None = PARAM_SECTIONS): if supports_params is None: supports_params = set(PARAM_SECTIONS) else: @@ -145,7 +136,6 @@ def __init__( if sec in self._parsed_data: self._parsed_data[sec] = self._normalize_param_section(self._parsed_data[sec]) - # --- continue normal fastcore behavior --- for section in SECTIONS: self[section] = dedent_lines(self[section], split=False) @@ -214,23 +204,11 @@ def _parse_param_list(self, content, single_element_is_type=False): return params def _normalize_param_section(self, val: list[Parameter] | Any) -> dict[Parameter] | Any: - """ - Convert lists of `Parameter` objects into a dict or clean list. - """ - # Not a list? Then noop. - if not isinstance(val, list): - return val - - # Falsy value i.e. empty list? Then noop. - if not val: - return val - - # Lazy check, assumes if first value is a Parameter, all are. - if not isinstance(val[0], Parameter): - return val - - # Convert to dict[name -> Parameter] - return {p.name: p for p in val} + """Convert lists of `Parameter` objects into a dict or clean list.""" + if not isinstance(val, list): return val # Not a list? Then noop. + if not val: return val # Falsy value i.e. empty list? Then noop. + if not isinstance(val[0], Parameter): return val # Lazy check, assumes if first value is a Parameter, all are. + return {p.name: p for p in val} # Convert to dict[name -> Parameter] def _parse_summary(self): """Grab signature (if given) and summary""" From e20bbe4d1f56688eb84bf40da8e9cbff0102ee22 Mon Sep 17 00:00:00 2001 From: s01st Date: Tue, 25 Nov 2025 08:26:03 -0600 Subject: [PATCH 149/182] remove type annotations --- fastcore/docscrape.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/fastcore/docscrape.py b/fastcore/docscrape.py index 4b29fe3f..f414db72 100644 --- a/fastcore/docscrape.py +++ b/fastcore/docscrape.py @@ -25,7 +25,6 @@ from warnings import warn from collections import namedtuple from collections.abc import Mapping -from typing import Any __all__ = ['Parameter', 'NumpyDocString', 'dedent_lines'] @@ -106,37 +105,32 @@ class NumpyDocString(Mapping): sections['Returns'] = [] param_sections: set[str] = set(PARAM_SECTIONS) - def __init__(self, docstring, config=None, supported_sections: list[str] | None = SECTIONS, supports_params: set[str] | None = PARAM_SECTIONS): + def __init__(self, docstring, config=None, supported_sections = SECTIONS, supports_params = PARAM_SECTIONS): if supports_params is None: supports_params = set(PARAM_SECTIONS) else: missing = set(supports_params) - set(self.param_sections) for sec in missing: self.param_sections.add(sec) - # If None, set to default supported set - if supported_sections is None: supported_sections = set(SECTIONS) + + if supported_sections is None: supported_sections = set(SECTIONS) # If None, set to default supported set else: - # add missing to class variable - missing = set(supported_sections) - set(self.sections.keys()) + missing = set(supported_sections) - set(self.sections.keys()) # add missing to class variable for sec in missing: self.sections[sec] = [] - # --- original initialization --- docstring = textwrap.dedent(docstring).split('\n') self._doc = Reader(docstring) self._parsed_data = copy.deepcopy(self.sections) self._parse() - # --- fastcore default normalization --- self['Parameters'] = {o.name:o for o in self['Parameters']} if self['Returns']: self['Returns'] = self['Returns'][0] - # --- normalize ALL parameter-like sections --- for sec in supports_params: if sec in self._parsed_data: self._parsed_data[sec] = self._normalize_param_section(self._parsed_data[sec]) - # --- continue normal fastcore behavior --- for section in SECTIONS: self[section] = dedent_lines(self[section], split=False) @@ -203,7 +197,7 @@ def _parse_param_list(self, content, single_element_is_type=False): params.append(Parameter(arg_name, arg_type, desc)) return params - def _normalize_param_section(self, val: list[Parameter] | Any) -> dict[Parameter] | Any: + def _normalize_param_section(self, val): """Convert lists of `Parameter` objects into a dict or clean list.""" if not isinstance(val, list): return val # Not a list? Then noop. if not val: return val # Falsy value i.e. empty list? Then noop. From 7afff83d6852413fb0164180f3a87ef058d8748d Mon Sep 17 00:00:00 2001 From: s01st Date: Tue, 25 Nov 2025 08:26:27 -0600 Subject: [PATCH 150/182] remove lingering comments --- fastcore/docscrape.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fastcore/docscrape.py b/fastcore/docscrape.py index f414db72..11898c26 100644 --- a/fastcore/docscrape.py +++ b/fastcore/docscrape.py @@ -113,9 +113,9 @@ def __init__(self, docstring, config=None, supported_sections = SECTIONS, suppor for sec in missing: self.param_sections.add(sec) - if supported_sections is None: supported_sections = set(SECTIONS) # If None, set to default supported set + if supported_sections is None: supported_sections = set(SECTIONS) else: - missing = set(supported_sections) - set(self.sections.keys()) # add missing to class variable + missing = set(supported_sections) - set(self.sections.keys()) for sec in missing: self.sections[sec] = [] @@ -199,10 +199,10 @@ def _parse_param_list(self, content, single_element_is_type=False): def _normalize_param_section(self, val): """Convert lists of `Parameter` objects into a dict or clean list.""" - if not isinstance(val, list): return val # Not a list? Then noop. - if not val: return val # Falsy value i.e. empty list? Then noop. - if not isinstance(val[0], Parameter): return val # Lazy check, assumes if first value is a Parameter, all are. - return {p.name: p for p in val} # Convert to dict[name -> Parameter] + if not isinstance(val, list): return val + if not val: return val + if not isinstance(val[0], Parameter): return val + return {p.name: p for p in val} def _parse_summary(self): """Grab signature (if given) and summary""" From 459f7dbccb1686e8d4ef96514850c780636cc2a3 Mon Sep 17 00:00:00 2001 From: s01st Date: Tue, 25 Nov 2025 08:26:55 -0600 Subject: [PATCH 151/182] Futher reduced normalize param section function --- fastcore/docscrape.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fastcore/docscrape.py b/fastcore/docscrape.py index 11898c26..57ee053c 100644 --- a/fastcore/docscrape.py +++ b/fastcore/docscrape.py @@ -199,8 +199,7 @@ def _parse_param_list(self, content, single_element_is_type=False): def _normalize_param_section(self, val): """Convert lists of `Parameter` objects into a dict or clean list.""" - if not isinstance(val, list): return val - if not val: return val + if not isinstance(val, list) or not val: return val if not isinstance(val[0], Parameter): return val return {p.name: p for p in val} From 5b8ca619d3705cf30c205a883f2fb7158a2786b5 Mon Sep 17 00:00:00 2001 From: Erik Gaasedelen Date: Tue, 2 Dec 2025 22:10:57 -0800 Subject: [PATCH 152/182] add tests --- fastcore/xml.py | 2 +- nbs/09_xml.ipynb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fastcore/xml.py b/fastcore/xml.py index b1e3946f..31cf4967 100644 --- a/fastcore/xml.py +++ b/fastcore/xml.py @@ -188,7 +188,7 @@ def _to_xml(elm, lvl=0, indent=True, do_escape=True): stag = tag if attrs: - sattrs = ' '.join(_to_attr(k, v) for k, v in attrs.items() if v not in (False, None, '') and (k=='_' or k[-1]!='_')) + sattrs = ' '.join(_to_attr(k, v) for k, v in attrs.items() if v is not False and v is not None and (k=='_' or k[-1]!='_')) if sattrs: stag += f' {sattrs}' cltag = '' if is_void else f'' diff --git a/nbs/09_xml.ipynb b/nbs/09_xml.ipynb index bd16c3f1..02aed112 100644 --- a/nbs/09_xml.ipynb +++ b/nbs/09_xml.ipynb @@ -440,7 +440,7 @@ "\n", " stag = tag\n", " if attrs:\n", - " sattrs = ' '.join(_to_attr(k, v) for k, v in attrs.items() if v not in (False, None, '') and (k=='_' or k[-1]!='_'))\n", + " sattrs = ' '.join(_to_attr(k, v) for k, v in attrs.items() if v is not False and v is not None and (k=='_' or k[-1]!='_'))\n", " if sattrs: stag += f' {sattrs}'\n", "\n", " cltag = '' if is_void else f''\n", @@ -511,7 +511,9 @@ " \"
<script>alert('XSS')</script>
\\n\")\n", "test_eq(to_xml(Div(\"\"), do_escape=False),\n", " \"
\\n\")\n", - "test_eq(to_xml(Div(foo=False), indent=False), '
')" + "test_eq(to_xml(Div(foo=False), indent=False), '
')\n", + "test_eq(to_xml(Input(type=\"number\", min=0, max=100)), '\\n')\n", + "test_eq(to_xml(Option(\"select a value\", value=\"\")), '')" ] }, { From cf6eed255efb98cbbe9b06d17278457bc9613829 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 8 Dec 2025 09:43:27 +1000 Subject: [PATCH 153/182] fixes #711 --- fastcore/_modidx.py | 1 + fastcore/xtras.py | 34 +++++++++++++++++++++++++----- nbs/03_xtras.ipynb | 51 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index c8f08b97..62300c00 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -658,6 +658,7 @@ 'fastcore.xtras.bunzip': ('xtras.html#bunzip', 'fastcore/xtras.py'), 'fastcore.xtras.console_help': ('xtras.html#console_help', 'fastcore/xtras.py'), 'fastcore.xtras.dataclass_src': ('xtras.html#dataclass_src', 'fastcore/xtras.py'), + 'fastcore.xtras.detect_mime': ('xtras.html#detect_mime', 'fastcore/xtras.py'), 'fastcore.xtras.dict2obj': ('xtras.html#dict2obj', 'fastcore/xtras.py'), 'fastcore.xtras.dumps': ('xtras.html#dumps', 'fastcore/xtras.py'), 'fastcore.xtras.exec_eval': ('xtras.html#exec_eval', 'fastcore/xtras.py'), diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 8dcb5697..cc3c8485 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -6,11 +6,11 @@ from __future__ import annotations # %% auto 0 -__all__ = ['spark_chars', 'UNSET', 'walk', 'globtastic', 'maybe_open', 'mkdir', 'image_size', 'img_bytes', 'bunzip', 'loads', - 'loads_multi', 'dumps', 'untar_dir', 'repo_details', 'shell', 'ssh', 'rsync_multi', 'run', 'open_file', - 'save_pickle', 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', - 'is_listy', 'mapped', 'IterLen', 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', - 'friendly_name', 'n_friendly_names', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', +__all__ = ['spark_chars', 'UNSET', 'walk', 'globtastic', 'maybe_open', 'mkdir', 'image_size', 'img_bytes', 'detect_mime', + 'bunzip', 'loads', 'loads_multi', 'dumps', 'untar_dir', 'repo_details', 'shell', 'ssh', 'rsync_multi', 'run', + 'open_file', 'save_pickle', 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', + 'repr_dict', 'is_listy', 'mapped', 'IterLen', 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', + 'asave_iter', 'friendly_name', 'n_friendly_names', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', @@ -134,6 +134,30 @@ def img_bytes(img, fmt='PNG'): img.save(buf, format=fmt) return buf.getvalue() +# %% ../nbs/03_xtras.ipynb +_sigs = { + (b'%PDF', 0): 'application/pdf', + (b'RIFF', 0): lambda d: 'audio/wav' if d[8:12]==b'WAVE' else 'video/avi' if d[8:12]==b'AVI ' else None, + (b'ID3', 0): 'audio/mp3', + (b'\xff\xfb', 0): 'audio/mp3', + (b'\xff\xf3', 0): 'audio/mp3', + (b'FORM', 0): lambda d: 'audio/aiff' if d[8:12]==b'AIFF' else None, + (b'OggS', 0): 'audio/ogg', + (b'fLaC', 0): 'audio/flac', + (b'ftyp', 4): lambda d: 'video/3gpp' if d[8:11]==b'3gp' else 'video/mp4', + (b'\x1a\x45\xdf', 0): 'video/webm', + (b'FLV', 0): 'video/x-flv', + (b'\x30\x26\xb2\x75', 0): 'video/wmv', + (b'\x00\x00\x01\xb3', 0): 'video/mpeg', +} + +def detect_mime(data): + "Get the MIME type for bytes `data`, covering common PDF, audio, video, and image types" + import mimetypes + for (sig,pos),mime in _sigs.items(): + if data[pos:pos+len(sig)]==sig: return mime(data) if callable(mime) else mime + return mimetypes.types_map.get(f'.{imghdr.what(None, h=data)}') + # %% ../nbs/03_xtras.ipynb def bunzip(fn): "bunzip `fn`, raising exception if output already exists" diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 6ef38b76..2f84492e 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -505,6 +505,57 @@ "IPImage(ib)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "_sigs = {\n", + " (b'%PDF', 0): 'application/pdf',\n", + " (b'RIFF', 0): lambda d: 'audio/wav' if d[8:12]==b'WAVE' else 'video/avi' if d[8:12]==b'AVI ' else None,\n", + " (b'ID3', 0): 'audio/mp3',\n", + " (b'\\xff\\xfb', 0): 'audio/mp3',\n", + " (b'\\xff\\xf3', 0): 'audio/mp3',\n", + " (b'FORM', 0): lambda d: 'audio/aiff' if d[8:12]==b'AIFF' else None,\n", + " (b'OggS', 0): 'audio/ogg',\n", + " (b'fLaC', 0): 'audio/flac',\n", + " (b'ftyp', 4): lambda d: 'video/3gpp' if d[8:11]==b'3gp' else 'video/mp4',\n", + " (b'\\x1a\\x45\\xdf', 0): 'video/webm',\n", + " (b'FLV', 0): 'video/x-flv',\n", + " (b'\\x30\\x26\\xb2\\x75', 0): 'video/wmv',\n", + " (b'\\x00\\x00\\x01\\xb3', 0): 'video/mpeg',\n", + "}\n", + "\n", + "def detect_mime(data):\n", + " \"Get the MIME type for bytes `data`, covering common PDF, audio, video, and image types\"\n", + " import mimetypes\n", + " for (sig,pos),mime in _sigs.items():\n", + " if data[pos:pos+len(sig)]==sig: return mime(data) if callable(mime) else mime\n", + " return mimetypes.types_map.get(f'.{imghdr.what(None, h=data)}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'image/png'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "detect_mime(ib)" + ] + }, { "cell_type": "code", "execution_count": null, From 37af26c51805ea18bfd4df217b21ed9f5fb6c5b9 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 8 Dec 2025 09:47:34 +1000 Subject: [PATCH 154/182] release --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f7ba58..793996db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ +## 1.8.17 + +### New Features + +- Add `detect_mime`, thanks to @KeremTurgutlu ([#711](https://github.com/AnswerDotAI/fastcore/issues/711)) +- Support setting zero and empty string as element attrs ([#709](https://github.com/AnswerDotAI/fastcore/pull/709)), thanks to [@erikgaas](https://github.com/erikgaas) +- Add `img_bytes` ([#707](https://github.com/AnswerDotAI/fastcore/issues/707)) + + ## 1.8.16 ### New Features From 071cf0b81c2bb9eb65f6fd6b759897f32d9d2002 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Mon, 8 Dec 2025 09:47:47 +1000 Subject: [PATCH 155/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index bae6b8a6..91c1a023 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.17" +__version__ = "1.8.18" diff --git a/settings.ini b/settings.ini index 076a9735..2f52e383 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.17 +version = 1.8.18 min_python = 3.10 audience = Developers language = English From b476a71492165838715e03a90bad6e3ca0985944 Mon Sep 17 00:00:00 2001 From: Kerem Turgutlu Date: Mon, 8 Dec 2025 11:19:27 +0300 Subject: [PATCH 156/182] fix detect_mime missing import --- fastcore/xtras.py | 1 + nbs/03_xtras.ipynb | 1 + 2 files changed, 2 insertions(+) diff --git a/fastcore/xtras.py b/fastcore/xtras.py index cc3c8485..12f21f07 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -154,6 +154,7 @@ def img_bytes(img, fmt='PNG'): def detect_mime(data): "Get the MIME type for bytes `data`, covering common PDF, audio, video, and image types" import mimetypes + from fastcore import imghdr for (sig,pos),mime in _sigs.items(): if data[pos:pos+len(sig)]==sig: return mime(data) if callable(mime) else mime return mimetypes.types_map.get(f'.{imghdr.what(None, h=data)}') diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 2f84492e..41247b71 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -531,6 +531,7 @@ "def detect_mime(data):\n", " \"Get the MIME type for bytes `data`, covering common PDF, audio, video, and image types\"\n", " import mimetypes\n", + " from fastcore import imghdr\n", " for (sig,pos),mime in _sigs.items():\n", " if data[pos:pos+len(sig)]==sig: return mime(data) if callable(mime) else mime\n", " return mimetypes.types_map.get(f'.{imghdr.what(None, h=data)}')" From fa1098ab54b8a99291384bdb7930e3c188f110bc Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 10:37:10 +1000 Subject: [PATCH 157/182] refactor L to pull out methods into separate cells --- fastcore/foundation.py | 270 ++++--- nbs/02_foundation.ipynb | 1468 ++++++++++++++++----------------------- nbs/03_xtras.ipynb | 297 +++++++- nbs/12_tools.ipynb | 8 +- 4 files changed, 1063 insertions(+), 980 deletions(-) diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 0ef7f7db..71a17c83 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -117,9 +117,9 @@ def __init__(self, items=None, *rest, use_list=False, match=None): def _xtra(self): return None def _new(self, items, *args, **kwargs): return type(self)(items, *args, use_list=None, **kwargs) def __getitem__(self, idx): + "Retrieve `idx` (can be list of indices, or mask, or int) items" if isinstance(idx,int) and not hasattr(self.items,'iloc'): return self.items[idx] return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None) - def copy(self): return self._new(self.items.copy()) def _get(self, i): if is_indexer(i) or isinstance(i,slice): return getattr(self.items,'iloc',self.items)[i] @@ -143,7 +143,6 @@ def __eq__(self,b): if isinstance(b, (str,dict)) or callable(b): return False return all_equal(b,self) - def sorted(self, key=None, reverse=False, cmp=None, **kwargs): return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs)) def __iter__(self): return iter(self.items.itertuples() if hasattr(self.items,'iloc') else self.items) def __contains__(self,b): return b in self.items def __reversed__(self): return self._new(reversed(self.items)) @@ -158,88 +157,6 @@ def __addi__(a,b): a.items += list(b) return a - @classmethod - def split(cls, s, sep=None, maxsplit=-1): return cls(s.split(sep,maxsplit)) - @classmethod - def splitlines(cls, s, keepends=False): return cls(s.splitlines(keepends)) - @classmethod - def range(cls, a, b=None, step=None): return cls(range_of(a, b=b, step=step)) - - def map(self, f, *args, **kwargs): return self._new(map_ex(self, f, *args, gen=False, **kwargs)) - def argwhere(self, f, negate=False, **kwargs): return self._new(argwhere(self, f, negate, **kwargs)) - def argfirst(self, f, negate=False): - if negate: f = not_(f) - return first(i for i,o in self.enumerate() if f(o)) - def filter(self, f=noop, negate=False, **kwargs): - return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs)) - - def enumerate(self): return L(enumerate(self)) - def renumerate(self): return L(renumerate(self)) - def unique(self, sort=False, bidir=False, start=None): return L(uniqueify(self, sort=sort, bidir=bidir, start=start)) - def val2idx(self): return val2idx(self) - def cycle(self): return cycle(self) - def groupby(self, key, val=noop): return groupby(self, key, val=val) - def map_dict(self, f=noop, *args, **kwargs): return {k:f(k, *args,**kwargs) for k in self} - def map_first(self, f=noop, g=noop, *args, **kwargs): - return first(self.map(f, *args, **kwargs), g) - - def itemgot(self, *idxs): - x = self - for idx in idxs: x = x.map(itemgetter(idx)) - return x - def attrgot(self, k, default=None): - return self.map(lambda o: o.get(k,default) if isinstance(o, dict) else nested_attr(o,k,default)) - - def starmap(self, f, *args, **kwargs): return self._new(itertools.starmap(partial(f,*args,**kwargs), self)) - def zip(self, cycled=False): return self._new((zip_cycle if cycled else zip)(*self)) - def zipwith(self, *rest, cycled=False): return self._new([self, *rest]).zip(cycled=cycled) - def map_zip(self, f, *args, cycled=False, **kwargs): return self.zip(cycled=cycled).starmap(f, *args, **kwargs) - def map_zipwith(self, f, *rest, cycled=False, **kwargs): return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs) - def shuffle(self): - it = copy(self.items) - random.shuffle(it) - return self._new(it) - - def concat(self): return self._new(itertools.chain.from_iterable(self.map(L))) - def reduce(self, f, initial=None): return reduce(f, self) if initial is None else reduce(f, self, initial) - def sum(self): return self.reduce(operator.add, 0) - def product(self): return self.reduce(operator.mul, 1) - def setattrs(self, attr, val): [setattr(o,attr,val) for o in self] - -# %% ../nbs/02_foundation.ipynb -add_docs(L, - __getitem__="Retrieve `idx` (can be list of indices, or mask, or int) items", - range="Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`", - split="Class Method: Same as `str.split`, but returns an `L`", - splitlines="Class Method: Same as `str.splitlines`, but returns an `L`", - copy="Same as `list.copy`, but returns an `L`", - sorted="New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`", - unique="Unique items, in stable order", - val2idx="Dict from value to index", - filter="Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`", - argwhere="Like `filter`, but return indices for matching items", - argfirst="Return index of first matching item", - map="Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`", - map_first="First element of `map_filter`", - map_dict="Like `map`, but creates a dict from `items` to function results", - starmap="Like `map`, but use `itertools.starmap`", - itemgot="Create new `L` with item `idx` of all `items`", - attrgot="Create new `L` with attr `k` (or value `k` for dicts) of all `items`.", - cycle="Same as `itertools.cycle`", - enumerate="Same as `enumerate`", - renumerate="Same as `renumerate`", - groupby="Same as `fastcore.basics.groupby`", - zip="Create new `L` with `zip(*items)`", - zipwith="Create new `L` with `self` zip with each of `*rest`", - map_zip="Combine `zip` and `starmap`", - map_zipwith="Combine `zipwith` and `starmap`", - concat="Concatenate all elements of list", - shuffle="Same as `random.shuffle`, but not inplace", - reduce="Wrapper for `functools.reduce`", - sum="Sum of the items", - product="Product of the items", - setattrs="Call `setattr` on all items") - # %% ../nbs/02_foundation.ipynb # Here we are fixing the signature of L. What happens is that the __call__ method on the MetaClass of L shadows the __init__ # giving the wrong signature (https://stackoverflow.com/questions/49740290/call-from-metaclass-shadows-signature-of-init). @@ -249,6 +166,191 @@ def _f(items=None, *rest, use_list=False, match=None): ... # %% ../nbs/02_foundation.ipynb Sequence.register(L); +# %% ../nbs/02_foundation.ipynb +@patch +def unique(self:L, sort=False, bidir=False, start=None): + "Unique items, in stable order" + return L(uniqueify(self, sort=sort, bidir=bidir, start=start)) + +# %% ../nbs/02_foundation.ipynb +@patch +def val2idx(self:L): + "Dict from value to index" + return val2idx(self) + +# %% ../nbs/02_foundation.ipynb +@patch(cls_method=True) +def split(cls:L, s, sep=None, maxsplit=-1): + "Class Method: Same as `str.split`, but returns an `L`" + return cls(s.split(sep,maxsplit)) + +# %% ../nbs/02_foundation.ipynb +@patch(cls_method=True) +def splitlines(cls:L, s, keepends=False): + "Class Method: Same as `str.splitlines`, but returns an `L`" + return cls(s.splitlines(keepends)) + +# %% ../nbs/02_foundation.ipynb +@patch +def groupby(self:L, key, val=noop): + "Same as `fastcore.basics.groupby`" + return groupby(self, key, val=val) + +# %% ../nbs/02_foundation.ipynb +@patch +def filter(self:L, f=noop, negate=False, **kwargs): + "Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`" + return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs)) + +# %% ../nbs/02_foundation.ipynb +@patch(cls_method=True) +def range(cls:L, a, b=None, step=None): + "Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`" + return cls(range_of(a, b=b, step=step)) + +# %% ../nbs/02_foundation.ipynb +@patch +def argwhere(self:L, f, negate=False, **kwargs): + "Like `filter`, but return indices for matching items" + return self._new(argwhere(self, f, negate, **kwargs)) + +# %% ../nbs/02_foundation.ipynb +@patch +def enumerate(self:L): + "Same as `enumerate`" + return L(enumerate(self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def renumerate(self:L): + "Same as `renumerate`" + return L(renumerate(self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def argfirst(self:L, f, negate=False): + "Return index of first matching item" + if negate: f = not_(f) + return first(i for i,o in self.enumerate() if f(o)) + +# %% ../nbs/02_foundation.ipynb +@patch +def map(self:L, f, *args, **kwargs): + "Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`" + return self._new(map_ex(self, f, *args, gen=False, **kwargs)) + +# %% ../nbs/02_foundation.ipynb +@patch +def starmap(self:L, f, *args, **kwargs): + "Like `map`, but use `itertools.starmap`" + return self._new(itertools.starmap(partial(f,*args,**kwargs), self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def map_dict(self:L, f=noop, *args, **kwargs): + "Like `map`, but creates a dict from `items` to function results" + return {k:f(k, *args,**kwargs) for k in self} + +# %% ../nbs/02_foundation.ipynb +@patch +def zip(self:L, cycled=False): + "Create new `L` with `zip(*items)`" + return self._new((zip_cycle if cycled else zip)(*self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def map_zip(self:L, f, *args, cycled=False, **kwargs): + "Combine `zip` and `starmap`" + return self.zip(cycled=cycled).starmap(f, *args, **kwargs) + +# %% ../nbs/02_foundation.ipynb +@patch +def zipwith(self:L, *rest, cycled=False): + "Create new `L` with `self` zip with each of `*rest`" + return self._new([self, *rest]).zip(cycled=cycled) + +# %% ../nbs/02_foundation.ipynb +@patch +def map_zipwith(self:L, f, *rest, cycled=False, **kwargs): + "Combine `zipwith` and `starmap`" + return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs) + +# %% ../nbs/02_foundation.ipynb +@patch +def itemgot(self:L, *idxs): + "Create new `L` with item `idx` of all `items`" + x = self + for idx in idxs: x = x.map(itemgetter(idx)) + return x + +# %% ../nbs/02_foundation.ipynb +@patch +def attrgot(self:L, k, default=None): + "Create new `L` with attr `k` (or value `k` for dicts) of all `items`." + return self.map(lambda o: o.get(k,default) if isinstance(o, dict) else nested_attr(o,k,default)) + +# %% ../nbs/02_foundation.ipynb +@patch +def sorted(self:L, key=None, reverse=False, cmp=None, **kwargs): + "New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`" + return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs)) + +# %% ../nbs/02_foundation.ipynb +@patch +def concat(self:L): + "Concatenate all elements of list" + return self._new(itertools.chain.from_iterable(self.map(L))) + +# %% ../nbs/02_foundation.ipynb +@patch +def copy(self:L): + "Same as `list.copy`, but returns an `L`" + return self._new(self.items.copy()) + +# %% ../nbs/02_foundation.ipynb +@patch +def cycle(self:L): + "Same as `itertools.cycle`" + return cycle(self) + +# %% ../nbs/02_foundation.ipynb +@patch +def shuffle(self:L): + "Same as `random.shuffle`, but not inplace" + it = copy(self.items) + random.shuffle(it) + return self._new(it) + +# %% ../nbs/02_foundation.ipynb +@patch +def reduce(self:L, f, initial=None): + "Wrapper for `functools.reduce`" + return reduce(f, self) if initial is None else reduce(f, self, initial) + +# %% ../nbs/02_foundation.ipynb +@patch +def sum(self:L): + "Sum of the items" + return self.reduce(operator.add, 0) + +# %% ../nbs/02_foundation.ipynb +@patch +def product(self:L): + "Product of the items" + return self.reduce(operator.mul, 1) + +# %% ../nbs/02_foundation.ipynb +@patch +def map_first(self:L, f=noop, g=noop, *args, **kwargs): + "First element of `map_filter`" + return first(self.map(f, *args, **kwargs), g) + +# %% ../nbs/02_foundation.ipynb +@patch +def setattrs(self:L, attr, val): + "Call `setattr` on all items" + [setattr(o,attr,val) for o in self] + # %% ../nbs/02_foundation.ipynb def save_config_file(file, d, **kwargs): "Write settings dict to a new config file, or overwrite the existing one." diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 954435c9..b7c26a87 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -3,6 +3,7 @@ { "cell_type": "code", "execution_count": null, + "id": "50bae063", "metadata": {}, "outputs": [], "source": [ @@ -12,6 +13,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0f974791", "metadata": {}, "outputs": [], "source": [ @@ -28,6 +30,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8990087b", "metadata": {}, "outputs": [], "source": [ @@ -39,6 +42,7 @@ }, { "cell_type": "markdown", + "id": "e58a0238", "metadata": {}, "source": [ "# Foundation\n", @@ -48,6 +52,7 @@ }, { "cell_type": "markdown", + "id": "13ac17ff", "metadata": {}, "source": [ "## Foundational Functions" @@ -56,6 +61,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f059df0f", "metadata": {}, "outputs": [], "source": [ @@ -72,6 +78,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c3fd027f", "metadata": {}, "outputs": [], "source": [ @@ -92,6 +99,7 @@ }, { "cell_type": "markdown", + "id": "620dc00e", "metadata": {}, "source": [ "`add_docs` allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in [our style guide](https://docs.fast.ai/dev/style.html).\n", @@ -102,6 +110,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0832c567", "metadata": {}, "outputs": [], "source": [ @@ -112,6 +121,7 @@ }, { "cell_type": "markdown", + "id": "adedd47e", "metadata": {}, "source": [ "You can add documentation to this class like so:" @@ -120,6 +130,7 @@ { "cell_type": "code", "execution_count": null, + "id": "da9d95f9", "metadata": {}, "outputs": [], "source": [ @@ -130,6 +141,7 @@ }, { "cell_type": "markdown", + "id": "a91ee046", "metadata": {}, "source": [ "Now, docstrings will appear as expected:" @@ -138,6 +150,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2c4a26aa", "metadata": {}, "outputs": [], "source": [ @@ -148,6 +161,7 @@ }, { "cell_type": "markdown", + "id": "47545904", "metadata": {}, "source": [ "`add_docs` also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:" @@ -156,6 +170,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a9f77cf6", "metadata": {}, "outputs": [], "source": [ @@ -170,6 +185,7 @@ { "cell_type": "code", "execution_count": null, + "id": "88805b49", "metadata": {}, "outputs": [], "source": [ @@ -188,6 +204,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b180313a", "metadata": {}, "outputs": [], "source": [ @@ -200,6 +217,7 @@ }, { "cell_type": "markdown", + "id": "e25bd40f", "metadata": {}, "source": [ "Instead of using `add_docs`, you can use the decorator `docs` as shown below. Note that the docstring for the class can be set with the argument `cls_doc`:" @@ -208,6 +226,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d10f59f9", "metadata": {}, "outputs": [], "source": [ @@ -228,6 +247,7 @@ }, { "cell_type": "markdown", + "id": "2ad622b1", "metadata": {}, "source": [ "For either the `docs` decorator or the `add_docs` function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the `_docs` attribute:" @@ -236,6 +256,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e4bbd1b9", "metadata": {}, "outputs": [], "source": [ @@ -253,6 +274,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0b1f9829", "metadata": {}, "outputs": [ { @@ -267,10 +289,6 @@ "*Test whether `o` can be used in a `for` loop*" ], "text/plain": [ - "---\n", - "\n", - "### is_iter\n", - "\n", "> is_iter (o)\n", "\n", "*Test whether `o` can be used in a `for` loop*" @@ -288,6 +306,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4e191b5b", "metadata": {}, "outputs": [], "source": [ @@ -300,6 +319,7 @@ { "cell_type": "code", "execution_count": null, + "id": "34a29b43", "metadata": {}, "outputs": [], "source": [ @@ -312,6 +332,7 @@ }, { "cell_type": "markdown", + "id": "198216f8", "metadata": {}, "source": [ "`coll_repr` is used to provide a more informative [`__repr__`](https://stackoverflow.com/questions/1984162/purpose-of-pythons-repr) about list-like objects. `coll_repr` and is used by `L` to build a `__repr__` that displays the length of a list in addition to a preview of a list.\n", @@ -322,6 +343,7 @@ { "cell_type": "code", "execution_count": null, + "id": "fe3ed954", "metadata": {}, "outputs": [], "source": [ @@ -334,6 +356,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6cf96f49", "metadata": {}, "outputs": [], "source": [ @@ -346,6 +369,7 @@ { "cell_type": "code", "execution_count": null, + "id": "66a28a53", "metadata": {}, "outputs": [], "source": [ @@ -364,6 +388,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3b7dd826", "metadata": {}, "outputs": [], "source": [ @@ -375,6 +400,7 @@ { "cell_type": "code", "execution_count": null, + "id": "af754019", "metadata": {}, "outputs": [], "source": [ @@ -388,6 +414,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7f8805d7", "metadata": {}, "outputs": [], "source": [ @@ -400,6 +427,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9346b91a", "metadata": {}, "outputs": [], "source": [ @@ -412,6 +440,7 @@ { "cell_type": "code", "execution_count": null, + "id": "21c554a1", "metadata": {}, "outputs": [], "source": [ @@ -421,6 +450,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8a281173", "metadata": {}, "outputs": [], "source": [ @@ -432,6 +462,7 @@ }, { "cell_type": "markdown", + "id": "2c4497d9", "metadata": {}, "source": [ "You can, for example index a single item in a list with an integer or a 0-dimensional numpy array:" @@ -440,6 +471,7 @@ { "cell_type": "code", "execution_count": null, + "id": "cf94cb54", "metadata": {}, "outputs": [], "source": [ @@ -449,6 +481,7 @@ }, { "cell_type": "markdown", + "id": "504d89e8", "metadata": {}, "source": [ "However, you cannot index into single item in a list with another list or a numpy array with ndim > 0. " @@ -457,6 +490,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1159335e", "metadata": {}, "outputs": [], "source": [ @@ -467,6 +501,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3fd3133f", "metadata": {}, "outputs": [], "source": [ @@ -479,6 +514,7 @@ { "cell_type": "code", "execution_count": null, + "id": "73a52d33", "metadata": {}, "outputs": [ { @@ -499,6 +535,7 @@ { "cell_type": "code", "execution_count": null, + "id": "fa5990de", "metadata": {}, "outputs": [ { @@ -519,6 +556,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6f9ef4e6", "metadata": {}, "outputs": [ { @@ -538,6 +576,7 @@ }, { "cell_type": "markdown", + "id": "d8a026cf", "metadata": {}, "source": [ "## `L` helpers" @@ -546,6 +585,7 @@ { "cell_type": "code", "execution_count": null, + "id": "eefd2763", "metadata": {}, "outputs": [], "source": [ @@ -563,6 +603,7 @@ }, { "cell_type": "markdown", + "id": "0ec47e38", "metadata": {}, "source": [ "`ColBase` is a base class that emulates the functionality of a python `list`:" @@ -571,6 +612,7 @@ { "cell_type": "code", "execution_count": null, + "id": "896956c4", "metadata": {}, "outputs": [], "source": [ @@ -586,6 +628,7 @@ }, { "cell_type": "markdown", + "id": "00b0d24f", "metadata": {}, "source": [ "## L -" @@ -594,6 +637,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d953eaf9", "metadata": {}, "outputs": [], "source": [ @@ -607,6 +651,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f571f489", "metadata": {}, "outputs": [], "source": [ @@ -623,9 +668,9 @@ " def _xtra(self): return None\n", " def _new(self, items, *args, **kwargs): return type(self)(items, *args, use_list=None, **kwargs)\n", " def __getitem__(self, idx):\n", + " \"Retrieve `idx` (can be list of indices, or mask, or int) items\"\n", " if isinstance(idx,int) and not hasattr(self.items,'iloc'): return self.items[idx]\n", " return self._get(idx) if is_indexer(idx) else L(self._get(idx), use_list=None)\n", - " def copy(self): return self._new(self.items.copy())\n", "\n", " def _get(self, i):\n", " if is_indexer(i) or isinstance(i,slice): return getattr(self.items,'iloc',self.items)[i]\n", @@ -649,7 +694,6 @@ " if isinstance(b, (str,dict)) or callable(b): return False\n", " return all_equal(b,self)\n", "\n", - " def sorted(self, key=None, reverse=False, cmp=None, **kwargs): return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs))\n", " def __iter__(self): return iter(self.items.itertuples() if hasattr(self.items,'iloc') else self.items)\n", " def __contains__(self,b): return b in self.items\n", " def __reversed__(self): return self._new(reversed(self.items))\n", @@ -662,101 +706,13 @@ " def __radd__(a,b): return a._new(b)+a\n", " def __addi__(a,b):\n", " a.items += list(b)\n", - " return a\n", - "\n", - " @classmethod\n", - " def split(cls, s, sep=None, maxsplit=-1): return cls(s.split(sep,maxsplit))\n", - " @classmethod\n", - " def splitlines(cls, s, keepends=False): return cls(s.splitlines(keepends))\n", - " @classmethod\n", - " def range(cls, a, b=None, step=None): return cls(range_of(a, b=b, step=step))\n", - "\n", - " def map(self, f, *args, **kwargs): return self._new(map_ex(self, f, *args, gen=False, **kwargs))\n", - " def argwhere(self, f, negate=False, **kwargs): return self._new(argwhere(self, f, negate, **kwargs))\n", - " def argfirst(self, f, negate=False): \n", - " if negate: f = not_(f)\n", - " return first(i for i,o in self.enumerate() if f(o))\n", - " def filter(self, f=noop, negate=False, **kwargs):\n", - " return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs))\n", - "\n", - " def enumerate(self): return L(enumerate(self))\n", - " def renumerate(self): return L(renumerate(self))\n", - " def unique(self, sort=False, bidir=False, start=None): return L(uniqueify(self, sort=sort, bidir=bidir, start=start))\n", - " def val2idx(self): return val2idx(self)\n", - " def cycle(self): return cycle(self)\n", - " def groupby(self, key, val=noop): return groupby(self, key, val=val)\n", - " def map_dict(self, f=noop, *args, **kwargs): return {k:f(k, *args,**kwargs) for k in self}\n", - " def map_first(self, f=noop, g=noop, *args, **kwargs):\n", - " return first(self.map(f, *args, **kwargs), g)\n", - "\n", - " def itemgot(self, *idxs):\n", - " x = self\n", - " for idx in idxs: x = x.map(itemgetter(idx))\n", - " return x\n", - " def attrgot(self, k, default=None):\n", - " return self.map(lambda o: o.get(k,default) if isinstance(o, dict) else nested_attr(o,k,default))\n", - "\n", - " def starmap(self, f, *args, **kwargs): return self._new(itertools.starmap(partial(f,*args,**kwargs), self))\n", - " def zip(self, cycled=False): return self._new((zip_cycle if cycled else zip)(*self))\n", - " def zipwith(self, *rest, cycled=False): return self._new([self, *rest]).zip(cycled=cycled)\n", - " def map_zip(self, f, *args, cycled=False, **kwargs): return self.zip(cycled=cycled).starmap(f, *args, **kwargs)\n", - " def map_zipwith(self, f, *rest, cycled=False, **kwargs): return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs)\n", - " def shuffle(self):\n", - " it = copy(self.items)\n", - " random.shuffle(it)\n", - " return self._new(it)\n", - "\n", - " def concat(self): return self._new(itertools.chain.from_iterable(self.map(L)))\n", - " def reduce(self, f, initial=None): return reduce(f, self) if initial is None else reduce(f, self, initial)\n", - " def sum(self): return self.reduce(operator.add, 0)\n", - " def product(self): return self.reduce(operator.mul, 1)\n", - " def setattrs(self, attr, val): [setattr(o,attr,val) for o in self]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "add_docs(L,\n", - " __getitem__=\"Retrieve `idx` (can be list of indices, or mask, or int) items\",\n", - " range=\"Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`\",\n", - " split=\"Class Method: Same as `str.split`, but returns an `L`\",\n", - " splitlines=\"Class Method: Same as `str.splitlines`, but returns an `L`\",\n", - " copy=\"Same as `list.copy`, but returns an `L`\",\n", - " sorted=\"New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`\",\n", - " unique=\"Unique items, in stable order\",\n", - " val2idx=\"Dict from value to index\",\n", - " filter=\"Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`\",\n", - " argwhere=\"Like `filter`, but return indices for matching items\",\n", - " argfirst=\"Return index of first matching item\",\n", - " map=\"Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`\",\n", - " map_first=\"First element of `map_filter`\",\n", - " map_dict=\"Like `map`, but creates a dict from `items` to function results\",\n", - " starmap=\"Like `map`, but use `itertools.starmap`\",\n", - " itemgot=\"Create new `L` with item `idx` of all `items`\",\n", - " attrgot=\"Create new `L` with attr `k` (or value `k` for dicts) of all `items`.\",\n", - " cycle=\"Same as `itertools.cycle`\",\n", - " enumerate=\"Same as `enumerate`\",\n", - " renumerate=\"Same as `renumerate`\",\n", - " groupby=\"Same as `fastcore.basics.groupby`\",\n", - " zip=\"Create new `L` with `zip(*items)`\",\n", - " zipwith=\"Create new `L` with `self` zip with each of `*rest`\",\n", - " map_zip=\"Combine `zip` and `starmap`\",\n", - " map_zipwith=\"Combine `zipwith` and `starmap`\",\n", - " concat=\"Concatenate all elements of list\",\n", - " shuffle=\"Same as `random.shuffle`, but not inplace\",\n", - " reduce=\"Wrapper for `functools.reduce`\",\n", - " sum=\"Sum of the items\",\n", - " product=\"Product of the items\",\n", - " setattrs=\"Call `setattr` on all items\")" + " return a" ] }, { "cell_type": "code", "execution_count": null, + "id": "05e35e65", "metadata": {}, "outputs": [], "source": [ @@ -771,8 +727,20 @@ { "cell_type": "code", "execution_count": null, + "id": "68e52115", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "__main__.L" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "#| export\n", "Sequence.register(L);" @@ -780,32 +748,25 @@ }, { "cell_type": "markdown", + "id": "0f12c35d", "metadata": {}, "source": [ - "`L` is a drop in replacement for a python `list`. Inspired by [NumPy](http://www.numpy.org/), `L`, supports advanced indexing and has additional methods (outlined below) that provide additional functionality and encourage simple expressive code. For example, the code below takes a list of pairs, selects the second item of each pair, takes its absolute value, filters items greater than 4, and adds them up:" + "`L` is a drop in replacement for a python `list`. Inspired by [NumPy](http://www.numpy.org/), `L`, supports advanced indexing and has additional methods (outlined below) that provide additional functionality and encourage simple expressive code." ] }, { "cell_type": "code", "execution_count": null, + "id": "a8a78e40", "metadata": {}, "outputs": [], "source": [ "from fastcore.utils import gt" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "d = dict(a=1,b=-5,d=6,e=9).items()\n", - "test_eq(L(d).itemgot(1).map(abs).filter(gt(4)).sum(), 20) # abs(-5) + abs(6) + abs(9) = 20; 1 was filtered out." - ] - }, { "cell_type": "markdown", + "id": "40393b8d", "metadata": {}, "source": [ "Read [this overview section](https://fastcore.fast.ai/tour.html#L) for a quick tutorial of `L`, as well as background on the name. \n", @@ -816,12 +777,13 @@ { "cell_type": "code", "execution_count": null, + "id": "72125daf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(#12) [11,10,9,'j',7,'k',5,4,3,2,1,0]" + "(#12) [0,1,2,'j',4,'k',6,7,8,9,10,11]" ] }, "execution_count": null, @@ -833,8 +795,6 @@ "t = L(range(12))\n", "test_eq(t, list(range(12)))\n", "test_ne(t, list(range(11)))\n", - "t.reverse()\n", - "test_eq(t[0], 11)\n", "t[3] = \"h\"\n", "test_eq(t[3], \"h\")\n", "t[3,5] = (\"j\",\"k\")\n", @@ -846,6 +806,7 @@ }, { "cell_type": "markdown", + "id": "694445fa", "metadata": {}, "source": [ "Any `L` is a `Sequence` so you can use it with methods like `random.sample`:" @@ -854,6 +815,7 @@ { "cell_type": "code", "execution_count": null, + "id": "03129273", "metadata": {}, "outputs": [], "source": [ @@ -863,6 +825,7 @@ { "cell_type": "code", "execution_count": null, + "id": "26bd9f4d", "metadata": {}, "outputs": [], "source": [ @@ -872,12 +835,13 @@ { "cell_type": "code", "execution_count": null, + "id": "6786d495", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[5, 0, 11]" + "[6, 11, 0]" ] }, "execution_count": null, @@ -893,6 +857,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3212eb40", "metadata": {}, "outputs": [], "source": [ @@ -909,6 +874,7 @@ }, { "cell_type": "markdown", + "id": "7a91eab9", "metadata": {}, "source": [ "There are optimized indexers for arrays, tensors, and DataFrames." @@ -917,6 +883,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7de0e777", "metadata": {}, "outputs": [], "source": [ @@ -926,6 +893,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2f32ddb9", "metadata": {}, "outputs": [], "source": [ @@ -940,6 +908,7 @@ }, { "cell_type": "markdown", + "id": "4bf55f8a", "metadata": {}, "source": [ "You can also modify an `L` with `append`, `+`, and `*`." @@ -948,6 +917,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8fc4ca2e", "metadata": {}, "outputs": [], "source": [ @@ -964,40 +934,12 @@ "test_eq(L(1,2,3), [1,2,3])\n", "test_eq(L(1,2,3), L(1,2,3))\n", "t = L(1)*5\n", - "t = t.map(operator.neg)\n", - "test_eq(t,[-1]*5)\n", - "test_eq(~L([True,False,False]), L([False,True,True]))\n", - "t = L(range(4))\n", - "test_eq(zip(t, L(1).cycle()), zip(range(4),(1,1,1,1)))\n", - "t = L.range(100)\n", - "test_shuffled(t,t.shuffle())" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(L([]).sum(), 0)\n", - "test_eq(L([]).product(), 1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def _f(x,a=0): return x+a\n", - "t = L(1)*5\n", - "test_eq(t.map(_f), t)\n", - "test_eq(t.map(_f,1), [2]*5)\n", - "test_eq(t.map(_f,a=2), [3]*5)" + "test_eq(~L([True,False,False]), L([False,True,True]))" ] }, { "cell_type": "markdown", + "id": "1c498ee5", "metadata": {}, "source": [ "An `L` can be constructed from anything iterable, although tensors and arrays will not be iterated over on construction, unless you pass `use_list` to the constructor." @@ -1006,6 +948,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c9d8144c", "metadata": {}, "outputs": [], "source": [ @@ -1023,6 +966,7 @@ }, { "cell_type": "markdown", + "id": "6b378062", "metadata": {}, "source": [ "If `match` is not `None` then the created list is same len as `match`, either by:\n", @@ -1034,6 +978,7 @@ { "cell_type": "code", "execution_count": null, + "id": "75d2daf4", "metadata": {}, "outputs": [], "source": [ @@ -1044,6 +989,7 @@ }, { "cell_type": "markdown", + "id": "c8153cf0", "metadata": {}, "source": [ "If you create an `L` from an existing `L` then you'll get back the original object (since `L` uses the `NewChkMeta` metaclass)." @@ -1052,6 +998,7 @@ { "cell_type": "code", "execution_count": null, + "id": "706d5ac7", "metadata": {}, "outputs": [], "source": [ @@ -1060,6 +1007,7 @@ }, { "cell_type": "markdown", + "id": "89d2c804", "metadata": {}, "source": [ "An `L` is considred equal to a list if they have the same elements. It's never considered equal to a `str` a `set` or a `dict` even if they have the same elements/keys." @@ -1068,6 +1016,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a208d40c", "metadata": {}, "outputs": [], "source": [ @@ -1078,6 +1027,7 @@ }, { "cell_type": "markdown", + "id": "91d995ad", "metadata": {}, "source": [ "### `L` Methods" @@ -1086,6 +1036,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8ba7bfe3", "metadata": {}, "outputs": [ { @@ -1093,7 +1044,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/foundation.py#L119){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.__getitem__\n", "\n", @@ -1102,12 +1053,6 @@ "*Retrieve `idx` (can be list of indices, or mask, or int) items*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L114){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.__getitem__\n", - "\n", "> L.__getitem__ (idx)\n", "\n", "*Retrieve `idx` (can be list of indices, or mask, or int) items*" @@ -1125,6 +1070,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a6edf562", "metadata": {}, "outputs": [], "source": [ @@ -1139,6 +1085,7 @@ { "cell_type": "code", "execution_count": null, + "id": "93ffea3b", "metadata": {}, "outputs": [ { @@ -1146,7 +1093,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L126){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/foundation.py#L131){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### L.__setitem__\n", "\n", @@ -1155,12 +1102,6 @@ "*Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable)*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L126){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.__setitem__\n", - "\n", "> L.__setitem__ (idx, o)\n", "\n", "*Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable)*" @@ -1178,6 +1119,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b84c56e3", "metadata": {}, "outputs": [], "source": [ @@ -1190,45 +1132,21 @@ { "cell_type": "code", "execution_count": null, + "id": "d966b525", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L173){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.unique\n", - "\n", - "> L.unique (sort=False, bidir=False, start=None)\n", - "\n", - "*Unique items, in stable order*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L173){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.unique\n", - "\n", - "> L.unique (sort=False, bidir=False, start=None)\n", - "\n", - "*Unique items, in stable order*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.unique)" + "#| export\n", + "@patch\n", + "def unique(self:L, sort=False, bidir=False, start=None):\n", + " \"Unique items, in stable order\"\n", + " return L(uniqueify(self, sort=sort, bidir=bidir, start=start))" ] }, { "cell_type": "code", "execution_count": null, + "id": "62aa9a1a", "metadata": {}, "outputs": [], "source": [ @@ -1238,45 +1156,21 @@ { "cell_type": "code", "execution_count": null, + "id": "840aef37", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L174){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.val2idx\n", - "\n", - "> L.val2idx ()\n", - "\n", - "*Dict from value to index*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L174){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.val2idx\n", - "\n", - "> L.val2idx ()\n", - "\n", - "*Dict from value to index*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.val2idx)" + "#| export\n", + "@patch\n", + "def val2idx(self:L):\n", + " \"Dict from value to index\"\n", + " return val2idx(self)" ] }, { "cell_type": "code", "execution_count": null, + "id": "76dd07e3", "metadata": {}, "outputs": [], "source": [ @@ -1286,197 +1180,119 @@ { "cell_type": "code", "execution_count": null, + "id": "13795373", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch(cls_method=True)\n", + "def split(cls:L, s, sep=None, maxsplit=-1):\n", + " \"Class Method: Same as `str.split`, but returns an `L`\"\n", + " return cls(s.split(sep,maxsplit))" + ] + }, + { + "cell_type": "markdown", + "id": "374f7aa9", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.groupby\n", - "\n", - "> L.groupby (key, val=)\n", - "\n", - "*Same as `fastcore.basics.groupby`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L176){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.groupby\n", - "\n", - "> L.groupby (key, val=)\n", - "\n", - "*Same as `fastcore.basics.groupby`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "show_doc(L.groupby)" + "`L.split` is a class method that works like `str.split`, but returns an `L` instead of a list:" ] }, { "cell_type": "code", "execution_count": null, + "id": "5b9583f3", "metadata": {}, "outputs": [], "source": [ - "words = L.split('aaa abc bba')\n", - "test_eq(words.groupby(0, (1,2)), {'a':[('a','a'),('b','c')], 'b':[('b','a')]})" + "test_eq(L.split('a b c'), ['a','b','c'])\n", + "test_eq(L.split('a-b-c', '-'), ['a','b','c'])\n", + "test_eq(L.split('a-b-c', '-', maxsplit=1), ['a','b-c'])" ] }, { "cell_type": "code", "execution_count": null, + "id": "6da7b43c", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L168){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.filter\n", - "\n", - "> L.filter (f=, negate=False, **kwargs)\n", - "\n", - "*Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L168){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.filter\n", - "\n", - "> L.filter (f=, negate=False, **kwargs)\n", - "\n", - "*Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.filter)" + "#| export\n", + "@patch(cls_method=True)\n", + "def splitlines(cls:L, s, keepends=False):\n", + " \"Class Method: Same as `str.splitlines`, but returns an `L`\"\n", + " return cls(s.splitlines(keepends))" + ] + }, + { + "cell_type": "markdown", + "id": "fd0e232b", + "metadata": {}, + "source": [ + "`L.splitlines` is a class method that works like `str.splitlines`, but returns an `L` instead of a list:" ] }, { "cell_type": "code", "execution_count": null, + "id": "2508910d", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[0, 1, 2, 3, 1, 5, 2, 7, 8, 9, 10, 11]" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "list(t)" + "test_eq(L.splitlines('a\\nb\\nc'), ['a','b','c'])\n", + "test_eq(L.splitlines('a\\nb\\nc', keepends=True), ['a\\n','b\\n','c'])" ] }, { "cell_type": "code", "execution_count": null, + "id": "148cc15c", "metadata": {}, "outputs": [], "source": [ - "test_eq(t.filter(lambda o:o<5), [0,1,2,3,1,2])\n", - "test_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11])" + "#| export\n", + "@patch\n", + "def groupby(self:L, key, val=noop):\n", + " \"Same as `fastcore.basics.groupby`\"\n", + " return groupby(self, key, val=val)" ] }, { "cell_type": "code", "execution_count": null, + "id": "4c88e24d", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L164){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.argwhere\n", - "\n", - "> L.argwhere (f, negate=False, **kwargs)\n", - "\n", - "*Like `filter`, but return indices for matching items*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L164){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.argwhere\n", - "\n", - "> L.argwhere (f, negate=False, **kwargs)\n", - "\n", - "*Like `filter`, but return indices for matching items*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.argwhere)" + "words = L.split('aaa abc bba')\n", + "test_eq(words.groupby(0, (1,2)), {'a':[('a','a'),('b','c')], 'b':[('b','a')]})" ] }, { "cell_type": "code", "execution_count": null, + "id": "66e76d03", "metadata": {}, "outputs": [], "source": [ - "test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6])" + "#| export\n", + "@patch\n", + "def filter(self:L, f=noop, negate=False, **kwargs):\n", + " \"Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`\"\n", + " return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs))" ] }, { "cell_type": "code", "execution_count": null, + "id": "79f76b77", "metadata": {}, "outputs": [ { "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L165){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.argfirst\n", - "\n", - "> L.argfirst (f, negate=False)\n", - "\n", - "*Return index of first matching item*" - ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L165){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.argfirst\n", - "\n", - "> L.argfirst (f, negate=False)\n", - "\n", - "*Return index of first matching item*" + "[0, 1, 2, 3, 1, 5, 2, 7, 8, 9, 10, 11]" ] }, "execution_count": null, @@ -1485,61 +1301,161 @@ } ], "source": [ - "show_doc(L.argfirst)" + "list(t)" ] }, { "cell_type": "code", "execution_count": null, + "id": "99fc6729", "metadata": {}, "outputs": [], "source": [ - "test_eq(t.argfirst(lambda o:o>4), 5)\n", - "test_eq(t.argfirst(lambda o:o>4,negate=True),0)" + "test_eq(t.filter(lambda o:o<5), [0,1,2,3,1,2])\n", + "test_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11])" ] }, { "cell_type": "code", "execution_count": null, + "id": "4b4a7bc4", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L163){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map\n", - "\n", - "> L.map (f, *args, **kwargs)\n", - "\n", - "*Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L163){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map\n", - "\n", - "> L.map (f, *args, **kwargs)\n", - "\n", - "*Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "source": [ + "#| export\n", + "@patch(cls_method=True)\n", + "def range(cls:L, a, b=None, step=None):\n", + " \"Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`\"\n", + " return cls(range_of(a, b=b, step=step))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42a5923c", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq_type(L.range([1,1,1]), L(range(3)))\n", + "test_eq_type(L.range(5,2,2), L(range(5,2,2)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2515ab8", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def argwhere(self:L, f, negate=False, **kwargs):\n", + " \"Like `filter`, but return indices for matching items\"\n", + " return self._new(argwhere(self, f, negate, **kwargs))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af037334", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32f6dbce", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def enumerate(self:L):\n", + " \"Same as `enumerate`\"\n", + " return L(enumerate(self))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "798d5bf2", + "metadata": {}, + "outputs": [], "source": [ - "show_doc(L.map)" + "test_eq(L('a','b','c').enumerate(), [(0,'a'),(1,'b'),(2,'c')])" ] }, { "cell_type": "code", "execution_count": null, + "id": "a10fa9d5", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def renumerate(self:L):\n", + " \"Same as `renumerate`\"\n", + " return L(renumerate(self))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5a49896", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L('a','b','c').renumerate(), [('a', 0), ('b', 1), ('c', 2)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e7fa556", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def argfirst(self:L, f, negate=False):\n", + " \"Return index of first matching item\"\n", + " if negate: f = not_(f)\n", + " return first(i for i,o in self.enumerate() if f(o))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eee16654", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(t.argfirst(lambda o:o>4), 5)\n", + "test_eq(t.argfirst(lambda o:o>4,negate=True),0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3979b19a", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def map(self:L, f, *args, **kwargs):\n", + " \"Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`\"\n", + " return self._new(map_ex(self, f, *args, gen=False, **kwargs))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2e6daf35", "metadata": {}, "outputs": [], "source": [ @@ -1548,6 +1464,7 @@ }, { "cell_type": "markdown", + "id": "a1c6950b", "metadata": {}, "source": [ "If `f` is a string then it is treated as a format string to create the mapping:" @@ -1556,6 +1473,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1169e4cb", "metadata": {}, "outputs": [], "source": [ @@ -1564,6 +1482,7 @@ }, { "cell_type": "markdown", + "id": "a27c80f8", "metadata": {}, "source": [ "If `f` is a dictionary (or anything supporting `__getitem__`) then it is indexed to create the mapping:" @@ -1572,6 +1491,7 @@ { "cell_type": "code", "execution_count": null, + "id": "64dcec5c", "metadata": {}, "outputs": [], "source": [ @@ -1580,6 +1500,7 @@ }, { "cell_type": "markdown", + "id": "004de9c3", "metadata": {}, "source": [ "You can also pass the same `arg` params that `bind` accepts:" @@ -1588,6 +1509,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ff80bb06", "metadata": {}, "outputs": [], "source": [ @@ -1598,45 +1520,54 @@ { "cell_type": "code", "execution_count": null, + "id": "dc2293d3", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def starmap(self:L, f, *args, **kwargs):\n", + " \"Like `map`, but use `itertools.starmap`\"\n", + " return self._new(itertools.starmap(partial(f,*args,**kwargs), self))" + ] + }, + { + "cell_type": "markdown", + "id": "bb8266d4", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L177){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map_dict\n", - "\n", - "> L.map_dict (f=, *args, **kwargs)\n", - "\n", - "*Like `map`, but creates a dict from `items` to function results*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L177){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map_dict\n", - "\n", - "> L.map_dict (f=, *args, **kwargs)\n", - "\n", - "*Like `map`, but creates a dict from `items` to function results*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "show_doc(L.map_dict)" + "`L.starmap` applies a function to each element, unpacking tuples as arguments:" ] }, { "cell_type": "code", "execution_count": null, + "id": "1f8fdde3", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L([(1,2),(3,4)]).starmap(operator.add), [3,7])\n", + "test_eq(L([(1,2,3),(4,5,6)]).starmap(lambda a,b,c: a+b*c), [7,34])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2356d414", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def map_dict(self:L, f=noop, *args, **kwargs):\n", + " \"Like `map`, but creates a dict from `items` to function results\"\n", + " return {k:f(k, *args,**kwargs) for k in self}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9539b84f", "metadata": {}, "outputs": [], "source": [ @@ -1647,45 +1578,21 @@ { "cell_type": "code", "execution_count": null, + "id": "27c2ec12", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L189){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.zip\n", - "\n", - "> L.zip (cycled=False)\n", - "\n", - "*Create new `L` with `zip(*items)`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L189){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.zip\n", - "\n", - "> L.zip (cycled=False)\n", - "\n", - "*Create new `L` with `zip(*items)`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.zip)" + "#| export\n", + "@patch\n", + "def zip(self:L, cycled=False):\n", + " \"Create new `L` with `zip(*items)`\"\n", + " return self._new((zip_cycle if cycled else zip)(*self))" ] }, { "cell_type": "code", "execution_count": null, + "id": "87f23d41", "metadata": {}, "outputs": [], "source": [ @@ -1696,6 +1603,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6d02ff1c", "metadata": {}, "outputs": [], "source": [ @@ -1707,45 +1615,21 @@ { "cell_type": "code", "execution_count": null, + "id": "9422eb7a", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L191){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map_zip\n", - "\n", - "> L.map_zip (f, *args, cycled=False, **kwargs)\n", - "\n", - "*Combine `zip` and `starmap`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L191){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map_zip\n", - "\n", - "> L.map_zip (f, *args, cycled=False, **kwargs)\n", - "\n", - "*Combine `zip` and `starmap`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.map_zip)" + "#| export\n", + "@patch\n", + "def map_zip(self:L, f, *args, cycled=False, **kwargs):\n", + " \"Combine `zip` and `starmap`\"\n", + " return self.zip(cycled=cycled).starmap(f, *args, **kwargs)" ] }, { "cell_type": "code", "execution_count": null, + "id": "6b41b405", "metadata": {}, "outputs": [], "source": [ @@ -1756,45 +1640,21 @@ { "cell_type": "code", "execution_count": null, + "id": "75969588", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L190){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.zipwith\n", - "\n", - "> L.zipwith (*rest, cycled=False)\n", - "\n", - "*Create new `L` with `self` zip with each of `*rest`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L190){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.zipwith\n", - "\n", - "> L.zipwith (*rest, cycled=False)\n", - "\n", - "*Create new `L` with `self` zip with each of `*rest`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.zipwith)" + "#| export\n", + "@patch\n", + "def zipwith(self:L, *rest, cycled=False):\n", + " \"Create new `L` with `self` zip with each of `*rest`\"\n", + " return self._new([self, *rest]).zip(cycled=cycled)" ] }, { "cell_type": "code", "execution_count": null, + "id": "33e21037", "metadata": {}, "outputs": [], "source": [ @@ -1806,437 +1666,318 @@ { "cell_type": "code", "execution_count": null, + "id": "89eca591", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L192){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map_zipwith\n", - "\n", - "> L.map_zipwith (f, *rest, cycled=False, **kwargs)\n", - "\n", - "*Combine `zipwith` and `starmap`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L192){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map_zipwith\n", - "\n", - "> L.map_zipwith (f, *rest, cycled=False, **kwargs)\n", - "\n", - "*Combine `zipwith` and `starmap`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def map_zipwith(self:L, f, *rest, cycled=False, **kwargs):\n", + " \"Combine `zipwith` and `starmap`\"\n", + " return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1af11e0e", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44055cd0", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def itemgot(self:L, *idxs):\n", + " \"Create new `L` with item `idx` of all `items`\"\n", + " x = self\n", + " for idx in idxs: x = x.map(itemgetter(idx))\n", + " return x" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "561a2f1a", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(t.itemgot(1), b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10f3d35e", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def attrgot(self:L, k, default=None):\n", + " \"Create new `L` with attr `k` (or value `k` for dicts) of all `items`.\"\n", + " return self.map(lambda o: o.get(k,default) if isinstance(o, dict) else nested_attr(o,k,default))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dad8526e", + "metadata": {}, + "outputs": [], + "source": [ + "# Example when items are not a dict\n", + "a = [SimpleNamespace(a=3,b=4),SimpleNamespace(a=1,b=2)]\n", + "test_eq(L(a).attrgot('b'), [4,2])\n", + "\n", + "#Example of when items are a dict\n", + "b =[{'id': 15, 'name': 'nbdev'}, {'id': 17, 'name': 'fastcore'}]\n", + "test_eq(L(b).attrgot('id'), [15, 17])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f238dfb", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def sorted(self:L, key=None, reverse=False, cmp=None, **kwargs):\n", + " \"New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`\"\n", + " return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "646cbc22", + "metadata": {}, + "outputs": [], "source": [ - "show_doc(L.map_zipwith)" + "test_eq(L(a).sorted('a').attrgot('b'), [2,4])" ] }, { "cell_type": "code", "execution_count": null, + "id": "9d86b896", "metadata": {}, "outputs": [], "source": [ - "test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12])" + "#| export\n", + "@patch\n", + "def concat(self:L):\n", + " \"Concatenate all elements of list\"\n", + " return self._new(itertools.chain.from_iterable(self.map(L)))" ] }, { "cell_type": "code", "execution_count": null, + "id": "6c74fb84", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L181){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.itemgot\n", - "\n", - "> L.itemgot (*idxs)\n", - "\n", - "*Create new `L` with item `idx` of all `items`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L181){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.itemgot\n", - "\n", - "> L.itemgot (*idxs)\n", - "\n", - "*Create new `L` with item `idx` of all `items`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.itemgot)" + "test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))" ] }, { "cell_type": "code", "execution_count": null, + "id": "b493aad4", "metadata": {}, "outputs": [], "source": [ - "test_eq(t.itemgot(1), b)" + "#| export\n", + "@patch\n", + "def copy(self:L):\n", + " \"Same as `list.copy`, but returns an `L`\"\n", + " return self._new(self.items.copy())" ] }, { "cell_type": "code", "execution_count": null, + "id": "b4aaf8fa", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L185){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.attrgot\n", - "\n", - "> L.attrgot (k, default=None)\n", - "\n", - "*Create new `L` with attr `k` (or value `k` for dicts) of all `items`.*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L185){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.attrgot\n", - "\n", - "> L.attrgot (k, default=None)\n", - "\n", - "*Create new `L` with attr `k` (or value `k` for dicts) of all `items`.*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.attrgot)" + "t = L([0,1,2,3],4,L(5,6)).copy()\n", + "test_eq(t.concat(), range(7))" ] }, { "cell_type": "code", "execution_count": null, + "id": "75215b78", "metadata": {}, "outputs": [], "source": [ - "# Example when items are not a dict\n", - "a = [SimpleNamespace(a=3,b=4),SimpleNamespace(a=1,b=2)]\n", - "test_eq(L(a).attrgot('b'), [4,2])\n", - "\n", - "#Example of when items are a dict\n", - "b =[{'id': 15, 'name': 'nbdev'}, {'id': 17, 'name': 'fastcore'}]\n", - "test_eq(L(b).attrgot('id'), [15, 17])" + "#| export\n", + "@patch\n", + "def cycle(self:L):\n", + " \"Same as `itertools.cycle`\"\n", + " return cycle(self)" ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", + "id": "275a5e35", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L141){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.sorted\n", - "\n", - "> L.sorted (key=None, reverse=False, cmp=None, **kwargs)\n", - "\n", - "*New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L141){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.sorted\n", - "\n", - "> L.sorted (key=None, reverse=False, cmp=None, **kwargs)\n", - "\n", - "*New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "show_doc(L.sorted)" + "`L.cycle` returns an infinite iterator that cycles through the elements:" ] }, { "cell_type": "code", "execution_count": null, + "id": "a34356a8", "metadata": {}, "outputs": [], "source": [ - "test_eq(L(a).sorted('a').attrgot('b'), [2,4])" + "test_eq(list(itertools.islice(L(1,2,3).cycle(), 7)), [1,2,3,1,2,3,1])" ] }, { "cell_type": "code", "execution_count": null, + "id": "7b2691d6", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def shuffle(self:L):\n", + " \"Same as `random.shuffle`, but not inplace\"\n", + " it = copy(self.items)\n", + " random.shuffle(it)\n", + " return self._new(it)" + ] + }, + { + "cell_type": "markdown", + "id": "30e80f78", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.split\n", - "\n", - "> L.split (s, sep=None, maxsplit=-1)\n", - "\n", - "*Class Method: Same as `str.split`, but returns an `L`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L157){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.split\n", - "\n", - "> L.split (s, sep=None, maxsplit=-1)\n", - "\n", - "*Class Method: Same as `str.split`, but returns an `L`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "show_doc(L.split)" + "`L.shuffle` returns a new shuffled `L`, leaving the original unchanged:" ] }, { "cell_type": "code", "execution_count": null, + "id": "3edd22b5", "metadata": {}, "outputs": [], "source": [ - "test_eq(L.split('a b c'), list('abc'))" + "t = L(1,2,3,4,5)\n", + "s = t.shuffle()\n", + "test_eq(set(s), set(t)) # same elements\n", + "test_eq(t, [1,2,3,4,5]) # original unchanged" ] }, { "cell_type": "code", "execution_count": null, + "id": "7b83c9e9", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.range\n", - "\n", - "> L.range (a, b=None, step=None)\n", - "\n", - "*Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L161){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.range\n", - "\n", - "> L.range (a, b=None, step=None)\n", - "\n", - "*Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.range)" + "#| export\n", + "@patch\n", + "def reduce(self:L, f, initial=None):\n", + " \"Wrapper for `functools.reduce`\"\n", + " return reduce(f, self) if initial is None else reduce(f, self, initial)" ] }, { "cell_type": "code", "execution_count": null, + "id": "bd4184ca", "metadata": {}, "outputs": [], "source": [ - "test_eq_type(L.range([1,1,1]), L(range(3)))\n", - "test_eq_type(L.range(5,2,2), L(range(5,2,2)))" + "test_eq(L(1,2,3,4).reduce(operator.add), 10)\n", + "test_eq(L(1,2,3,4).reduce(operator.mul, 10), 240)" ] }, { "cell_type": "code", "execution_count": null, + "id": "84b3413b", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L198){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.concat\n", - "\n", - "> L.concat ()\n", - "\n", - "*Concatenate all elements of list*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L198){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.concat\n", - "\n", - "> L.concat ()\n", - "\n", - "*Concatenate all elements of list*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.concat)" + "#| export\n", + "@patch\n", + "def sum(self:L):\n", + " \"Sum of the items\"\n", + " return self.reduce(operator.add, 0)" ] }, { "cell_type": "code", "execution_count": null, + "id": "18e6c336", "metadata": {}, "outputs": [], "source": [ - "test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))" + "test_eq(L(1,2,3,4).sum(), 10)\n", + "test_eq(L().sum(), 0)" ] }, { "cell_type": "code", "execution_count": null, + "id": "2a96a0fd", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L117){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.copy\n", - "\n", - "> L.copy ()\n", - "\n", - "*Same as `list.copy`, but returns an `L`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L117){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.copy\n", - "\n", - "> L.copy ()\n", - "\n", - "*Same as `list.copy`, but returns an `L`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.copy)" + "#| export\n", + "@patch\n", + "def product(self:L):\n", + " \"Product of the items\"\n", + " return self.reduce(operator.mul, 1)" ] }, { "cell_type": "code", "execution_count": null, + "id": "a41eb5c0", "metadata": {}, "outputs": [], "source": [ - "t = L([0,1,2,3],4,L(5,6)).copy()\n", - "test_eq(t.concat(), range(7))" + "test_eq(L(1,2,3,4).product(), 24)\n", + "test_eq(L().product(), 1)" ] }, { "cell_type": "code", "execution_count": null, + "id": "f82d9975", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L178){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map_first\n", - "\n", - "> L.map_first (f=, g=, *args, **kwargs)\n", - "\n", - "*First element of `map_filter`*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L178){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.map_first\n", - "\n", - "> L.map_first (f=, g=, *args, **kwargs)\n", - "\n", - "*First element of `map_filter`*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.map_first)" + "#| export\n", + "@patch\n", + "def map_first(self:L, f=noop, g=noop, *args, **kwargs):\n", + " \"First element of `map_filter`\"\n", + " return first(self.map(f, *args, **kwargs), g)" ] }, { "cell_type": "code", "execution_count": null, + "id": "6d29b6c6", "metadata": {}, "outputs": [], "source": [ @@ -2247,45 +1988,21 @@ { "cell_type": "code", "execution_count": null, + "id": "1366f0fa", "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L202){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.setattrs\n", - "\n", - "> L.setattrs (attr, val)\n", - "\n", - "*Call `setattr` on all items*" - ], - "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L202){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### L.setattrs\n", - "\n", - "> L.setattrs (attr, val)\n", - "\n", - "*Call `setattr` on all items*" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "show_doc(L.setattrs)" + "#| export\n", + "@patch\n", + "def setattrs(self:L, attr, val):\n", + " \"Call `setattr` on all items\"\n", + " [setattr(o,attr,val) for o in self]" ] }, { "cell_type": "code", "execution_count": null, + "id": "ca54000c", "metadata": {}, "outputs": [], "source": [ @@ -2296,6 +2013,7 @@ }, { "cell_type": "markdown", + "id": "8cf19e29", "metadata": {}, "source": [ "## Config" @@ -2304,6 +2022,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8e2bafde", "metadata": {}, "outputs": [], "source": [ @@ -2318,6 +2037,7 @@ { "cell_type": "code", "execution_count": null, + "id": "08333fdc", "metadata": {}, "outputs": [], "source": [ @@ -2330,6 +2050,7 @@ }, { "cell_type": "markdown", + "id": "8f55a699", "metadata": {}, "source": [ "Config files are saved and read using Python's `configparser.ConfigParser`, inside the `DEFAULT` section." @@ -2338,6 +2059,7 @@ { "cell_type": "code", "execution_count": null, + "id": "964cf19a", "metadata": {}, "outputs": [ { @@ -2367,6 +2089,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e06640e8", "metadata": {}, "outputs": [], "source": [ @@ -2416,6 +2139,7 @@ }, { "cell_type": "markdown", + "id": "22705fae", "metadata": {}, "source": [ "`Config` is a convenient wrapper around `ConfigParser` ini files with a single section (`DEFAULT`).\n", @@ -2426,6 +2150,7 @@ { "cell_type": "code", "execution_count": null, + "id": "219b1aec", "metadata": {}, "outputs": [ { @@ -2448,6 +2173,7 @@ }, { "cell_type": "markdown", + "id": "2cb7a98c", "metadata": {}, "source": [ "You can create a new file if one doesn't exist by providing a `create` dict:" @@ -2456,6 +2182,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d99a56cd", "metadata": {}, "outputs": [ { @@ -2477,6 +2204,7 @@ }, { "cell_type": "markdown", + "id": "417365fd", "metadata": {}, "source": [ "If you additionally pass `save=False`, the `Config` will contain the items from `create` without writing a new file:" @@ -2485,6 +2213,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1f7282b8", "metadata": {}, "outputs": [], "source": [ @@ -2495,6 +2224,7 @@ }, { "cell_type": "markdown", + "id": "33aae4e7", "metadata": {}, "source": [ "You can also pass in `ConfigParser` `kwargs` to change the behavior of how your configuration file will be parsed. For example, by default, inline comments are not handled by `Config`. However, if you pass in the `inline_comment_prefixes` with whatever your comment symbol is, you'll overwrite this behavior. " @@ -2503,6 +2233,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f8bd60db", "metadata": {}, "outputs": [], "source": [ @@ -2531,6 +2262,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5143b482", "metadata": {}, "outputs": [], "source": [ @@ -2544,6 +2276,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2341d0b5", "metadata": {}, "outputs": [ { @@ -2578,6 +2311,7 @@ }, { "cell_type": "markdown", + "id": "c6b2a144", "metadata": {}, "source": [ "Keys can be accessed as attributes, items, or with `get` and an optional default:" @@ -2586,6 +2320,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1f30f8eb", "metadata": {}, "outputs": [], "source": [ @@ -2596,6 +2331,7 @@ }, { "cell_type": "markdown", + "id": "dd6d6c95", "metadata": {}, "source": [ "Extra files can be read _before_ `cfg_path/cfg_name` using `extra_files`, in the order they appear:" @@ -2604,6 +2340,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6c124f0f", "metadata": {}, "outputs": [], "source": [ @@ -2616,6 +2353,7 @@ }, { "cell_type": "markdown", + "id": "4d3bee50", "metadata": {}, "source": [ "If you pass a dict `types`, then the values of that dict will be used as types to instantiate all values returned. `Path` is a special case -- in that case, the path returned will be relative to the path containing the config file (assuming the value is relative). `bool` types use `str2bool` to convert to boolean." @@ -2624,6 +2362,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c3ab1e1d", "metadata": {}, "outputs": [], "source": [ @@ -2638,6 +2377,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1542e621", "metadata": {}, "outputs": [ { @@ -2676,6 +2416,7 @@ }, { "cell_type": "markdown", + "id": "c6745351", "metadata": {}, "source": [ "You can use `Config.find` to search subdirectories for a config file, starting in the current path if no path is specified:" @@ -2684,6 +2425,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5f858954", "metadata": {}, "outputs": [ { @@ -2703,6 +2445,7 @@ }, { "cell_type": "markdown", + "id": "c856bdd5", "metadata": {}, "source": [ "# Export -" @@ -2711,6 +2454,7 @@ { "cell_type": "code", "execution_count": null, + "id": "68064341", "metadata": {}, "outputs": [], "source": [ @@ -2721,21 +2465,13 @@ { "cell_type": "code", "execution_count": null, + "id": "3ab162ca", "metadata": {}, "outputs": [], "source": [] } ], - "metadata": { - "jupytext": { - "split_at_heading": true - }, - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, + "metadata": {}, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 2f84492e..8b9b6592 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -3,6 +3,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2d19514c", "metadata": {}, "outputs": [], "source": [ @@ -12,6 +13,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ecd054a6", "metadata": {}, "outputs": [], "source": [ @@ -22,6 +24,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3401d507", "metadata": {}, "outputs": [], "source": [ @@ -43,6 +46,7 @@ { "cell_type": "code", "execution_count": null, + "id": "94c05ff5", "metadata": {}, "outputs": [], "source": [ @@ -58,6 +62,7 @@ }, { "cell_type": "markdown", + "id": "3e3da365", "metadata": {}, "source": [ "# Utility functions\n", @@ -67,6 +72,7 @@ }, { "cell_type": "markdown", + "id": "c8002f11", "metadata": {}, "source": [ "## File Functions" @@ -74,6 +80,7 @@ }, { "cell_type": "markdown", + "id": "565b8a1d", "metadata": {}, "source": [ "Utilities (other than extensions to Pathlib.Path) for dealing with IO." @@ -82,6 +89,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f64c2b09", "metadata": {}, "outputs": [], "source": [ @@ -110,6 +118,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b86f6f56", "metadata": {}, "outputs": [], "source": [ @@ -149,19 +158,9 @@ { "cell_type": "code", "execution_count": null, + "id": "91c9e1df", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(#5) ['./fastcore/basics.py','./fastcore/dispatch.py','./fastcore/docments.py','./fastcore/docscrape.py','./fastcore/script.py']" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "globtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c')" ] @@ -169,6 +168,7 @@ { "cell_type": "code", "execution_count": null, + "id": "93a1c35d", "metadata": {}, "outputs": [ { @@ -189,6 +189,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d514da68", "metadata": {}, "outputs": [], "source": [ @@ -203,6 +204,7 @@ }, { "cell_type": "markdown", + "id": "1b1e0c94", "metadata": {}, "source": [ "This is useful for functions where you want to accept a path *or* file. `maybe_open` will not close your file handle if you pass one in." @@ -211,6 +213,7 @@ { "cell_type": "code", "execution_count": null, + "id": "27e4f70d", "metadata": {}, "outputs": [], "source": [ @@ -225,6 +228,7 @@ }, { "cell_type": "markdown", + "id": "6e150f95", "metadata": {}, "source": [ "For example, we can use this to reimplement [`imghdr.what`](https://docs.python.org/3/library/imghdr.html#imghdr.what) from the Python standard library, which is [written in Python 3.9](https://github.com/python/cpython/blob/3.9/Lib/imghdr.py#L11) as:" @@ -233,6 +237,7 @@ { "cell_type": "code", "execution_count": null, + "id": "dbaa6878", "metadata": {}, "outputs": [], "source": [ @@ -242,6 +247,7 @@ { "cell_type": "code", "execution_count": null, + "id": "50e0c494", "metadata": {}, "outputs": [], "source": [ @@ -266,6 +272,7 @@ }, { "cell_type": "markdown", + "id": "61bbdf9d", "metadata": {}, "source": [ "Here's an example of the use of this function:" @@ -274,6 +281,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1a301e49", "metadata": {}, "outputs": [ { @@ -294,6 +302,7 @@ }, { "cell_type": "markdown", + "id": "63fd51fe", "metadata": {}, "source": [ "With `maybe_open`, `Self`, and `L.map_first`, we can rewrite this in a much more concise and (in our opinion) clear way:" @@ -302,6 +311,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2c1557f2", "metadata": {}, "outputs": [], "source": [ @@ -313,6 +323,7 @@ }, { "cell_type": "markdown", + "id": "a136809f", "metadata": {}, "source": [ "...and we can check that it still works:" @@ -321,6 +332,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a443b319", "metadata": {}, "outputs": [], "source": [ @@ -329,6 +341,7 @@ }, { "cell_type": "markdown", + "id": "00199093", "metadata": {}, "source": [ "...along with the version passing a file handle:" @@ -337,6 +350,7 @@ { "cell_type": "code", "execution_count": null, + "id": "05871c81", "metadata": {}, "outputs": [], "source": [ @@ -345,6 +359,7 @@ }, { "cell_type": "markdown", + "id": "0eed4b0f", "metadata": {}, "source": [ "...along with the `h` parameter version:" @@ -353,6 +368,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8377f8af", "metadata": {}, "outputs": [], "source": [ @@ -362,6 +378,7 @@ { "cell_type": "code", "execution_count": null, + "id": "63d86576", "metadata": {}, "outputs": [], "source": [ @@ -378,6 +395,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7ca6a87d", "metadata": {}, "outputs": [], "source": [ @@ -397,6 +415,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b33313ff", "metadata": {}, "outputs": [], "source": [ @@ -429,6 +448,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2e9a1760", "metadata": {}, "outputs": [], "source": [ @@ -438,6 +458,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c7dc50c4", "metadata": {}, "outputs": [], "source": [ @@ -448,6 +469,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1f3c35e2", "metadata": {}, "outputs": [ { @@ -471,6 +493,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d3aa6edf", "metadata": {}, "outputs": [], "source": [ @@ -486,6 +509,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3d8d71ae", "metadata": {}, "outputs": [ { @@ -508,6 +532,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e5801198", "metadata": {}, "outputs": [], "source": [ @@ -539,6 +564,7 @@ { "cell_type": "code", "execution_count": null, + "id": "95c46f5a", "metadata": {}, "outputs": [ { @@ -559,6 +585,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f856746a", "metadata": {}, "outputs": [], "source": [ @@ -577,6 +604,7 @@ { "cell_type": "code", "execution_count": null, + "id": "edd12907", "metadata": {}, "outputs": [], "source": [ @@ -592,6 +620,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0a78c637", "metadata": {}, "outputs": [], "source": [ @@ -607,6 +636,7 @@ { "cell_type": "code", "execution_count": null, + "id": "131170f7", "metadata": {}, "outputs": [], "source": [ @@ -626,6 +656,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2baed86c", "metadata": {}, "outputs": [], "source": [ @@ -644,6 +675,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ed688ac4", "metadata": {}, "outputs": [], "source": [ @@ -659,6 +691,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ac84300c", "metadata": {}, "outputs": [], "source": [ @@ -673,6 +706,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6f1e90e9", "metadata": {}, "outputs": [], "source": [ @@ -699,6 +733,7 @@ { "cell_type": "code", "execution_count": null, + "id": "39d4866a", "metadata": {}, "outputs": [], "source": [ @@ -714,6 +749,7 @@ }, { "cell_type": "markdown", + "id": "f57010a9", "metadata": {}, "source": [ "If the contents of `fname` contain just one file or directory, it is placed directly in `dest`:" @@ -722,6 +758,7 @@ { "cell_type": "code", "execution_count": null, + "id": "88501e83", "metadata": {}, "outputs": [], "source": [ @@ -731,6 +768,7 @@ }, { "cell_type": "markdown", + "id": "ba67bbc3", "metadata": {}, "source": [ "If `rename` then the directory created is named based on the archive, without extension:" @@ -739,6 +777,7 @@ { "cell_type": "code", "execution_count": null, + "id": "72e2d7a2", "metadata": {}, "outputs": [], "source": [ @@ -747,6 +786,7 @@ }, { "cell_type": "markdown", + "id": "e8937d1f", "metadata": {}, "source": [ "If the contents of `fname` contain multiple files and directories, a new folder in `dest` is created with the same name as `fname` (but without extension):" @@ -755,6 +795,7 @@ { "cell_type": "code", "execution_count": null, + "id": "16235244", "metadata": {}, "outputs": [], "source": [ @@ -765,6 +806,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ab91ee87", "metadata": {}, "outputs": [], "source": [ @@ -779,6 +821,7 @@ { "cell_type": "code", "execution_count": null, + "id": "20f50fcb", "metadata": {}, "outputs": [], "source": [ @@ -789,6 +832,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ba70e535", "metadata": {}, "outputs": [], "source": [ @@ -802,6 +846,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f62134a5", "metadata": {}, "outputs": [], "source": [ @@ -815,6 +860,7 @@ { "cell_type": "code", "execution_count": null, + "id": "cb24780e", "metadata": {}, "outputs": [], "source": [ @@ -829,6 +875,7 @@ { "cell_type": "code", "execution_count": null, + "id": "dda52646", "metadata": {}, "outputs": [], "source": [ @@ -859,6 +906,7 @@ }, { "cell_type": "markdown", + "id": "6704bb62", "metadata": {}, "source": [ "You can pass a string (which will be split based on standard shell rules), a list, or pass args directly:" @@ -867,6 +915,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8bcce474", "metadata": {}, "outputs": [ { @@ -889,6 +938,7 @@ { "cell_type": "code", "execution_count": null, + "id": "51a8302a", "metadata": {}, "outputs": [], "source": [ @@ -904,6 +954,7 @@ }, { "cell_type": "markdown", + "id": "f9cc74bc", "metadata": {}, "source": [ "Some commands fail in non-error situations, like `grep`. Use `ignore_ex` in those cases, which will return a tuple of stdout and returncode:" @@ -912,6 +963,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6567205d", "metadata": {}, "outputs": [], "source": [ @@ -923,6 +975,7 @@ }, { "cell_type": "markdown", + "id": "d235de1c", "metadata": {}, "source": [ "`run` automatically decodes returned bytes to a `str`. Use `as_bytes` to skip that:" @@ -931,6 +984,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2a41d502", "metadata": {}, "outputs": [], "source": [ @@ -943,6 +997,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5097b830", "metadata": {}, "outputs": [], "source": [ @@ -961,6 +1016,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a9fc7a40", "metadata": {}, "outputs": [], "source": [ @@ -974,6 +1030,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5e9bc30e", "metadata": {}, "outputs": [], "source": [ @@ -987,6 +1044,7 @@ { "cell_type": "code", "execution_count": null, + "id": "59d72612", "metadata": {}, "outputs": [], "source": [ @@ -1004,6 +1062,7 @@ { "cell_type": "code", "execution_count": null, + "id": "02029d02", "metadata": {}, "outputs": [], "source": [ @@ -1022,6 +1081,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4e176024", "metadata": {}, "outputs": [], "source": [ @@ -1042,6 +1102,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d030f939", "metadata": {}, "outputs": [], "source": [ @@ -1083,6 +1144,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3c593d73", "metadata": {}, "outputs": [], "source": [ @@ -1119,6 +1181,7 @@ }, { "cell_type": "markdown", + "id": "21654e3e", "metadata": {}, "source": [ "## Collections" @@ -1127,6 +1190,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7d7f3001", "metadata": {}, "outputs": [], "source": [ @@ -1140,6 +1204,7 @@ }, { "cell_type": "markdown", + "id": "810bd34d", "metadata": {}, "source": [ "This is a convenience to give you \"dotted\" access to (possibly nested) dictionaries, e.g:" @@ -1148,6 +1213,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f510433e", "metadata": {}, "outputs": [], "source": [ @@ -1159,6 +1225,7 @@ }, { "cell_type": "markdown", + "id": "ff43418b", "metadata": {}, "source": [ "It can also be used on lists of dicts." @@ -1167,6 +1234,7 @@ { "cell_type": "code", "execution_count": null, + "id": "90445bc3", "metadata": {}, "outputs": [], "source": [ @@ -1178,6 +1246,7 @@ { "cell_type": "code", "execution_count": null, + "id": "055a1e07", "metadata": {}, "outputs": [], "source": [ @@ -1191,6 +1260,7 @@ }, { "cell_type": "markdown", + "id": "48b4585c", "metadata": {}, "source": [ "`obj2dict` can be used to reverse what is done by `dict2obj`:" @@ -1199,6 +1269,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5118055b", "metadata": {}, "outputs": [], "source": [ @@ -1209,6 +1280,7 @@ { "cell_type": "code", "execution_count": null, + "id": "36b00c72", "metadata": {}, "outputs": [], "source": [ @@ -1224,6 +1296,7 @@ { "cell_type": "code", "execution_count": null, + "id": "13bb8fba", "metadata": {}, "outputs": [], "source": [ @@ -1236,6 +1309,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6d4f972b", "metadata": {}, "outputs": [ { @@ -1256,6 +1330,7 @@ { "cell_type": "code", "execution_count": null, + "id": "20842e1f", "metadata": {}, "outputs": [], "source": [ @@ -1268,6 +1343,7 @@ { "cell_type": "code", "execution_count": null, + "id": "fb1d1c14", "metadata": {}, "outputs": [], "source": [ @@ -1281,6 +1357,7 @@ { "cell_type": "code", "execution_count": null, + "id": "40c2c641", "metadata": {}, "outputs": [], "source": [ @@ -1293,6 +1370,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ca6571e1", "metadata": {}, "outputs": [], "source": [ @@ -1305,6 +1383,7 @@ }, { "cell_type": "markdown", + "id": "f15ac42d", "metadata": {}, "source": [ "## Extensions to Pathlib.Path" @@ -1312,6 +1391,7 @@ }, { "cell_type": "markdown", + "id": "c01cc75f", "metadata": {}, "source": [ "The following methods are added to the standard python libary [Pathlib.Path](https://docs.python.org/3/library/pathlib.html#basic-use)." @@ -1320,6 +1400,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ab0758e6", "metadata": {}, "outputs": [], "source": [ @@ -1333,6 +1414,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5ad0571d", "metadata": {}, "outputs": [], "source": [ @@ -1346,6 +1428,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d6d8d893", "metadata": {}, "outputs": [], "source": [ @@ -1361,6 +1444,7 @@ { "cell_type": "code", "execution_count": null, + "id": "233a8732", "metadata": {}, "outputs": [], "source": [ @@ -1374,6 +1458,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9fc9965f", "metadata": {}, "outputs": [], "source": [ @@ -1387,6 +1472,7 @@ { "cell_type": "code", "execution_count": null, + "id": "765fc8ea", "metadata": {}, "outputs": [ { @@ -1408,6 +1494,7 @@ { "cell_type": "code", "execution_count": null, + "id": "00f42ab6", "metadata": {}, "outputs": [ { @@ -1428,6 +1515,7 @@ { "cell_type": "code", "execution_count": null, + "id": "12264861", "metadata": {}, "outputs": [], "source": [ @@ -1446,6 +1534,7 @@ }, { "cell_type": "markdown", + "id": "47bf2fcb", "metadata": {}, "source": [ "We add an `ls()` method to `pathlib.Path` which is simply defined as `list(Path.iterdir())`, mainly for convenience in REPL environments such as notebooks." @@ -1454,6 +1543,7 @@ { "cell_type": "code", "execution_count": null, + "id": "197e61a6", "metadata": {}, "outputs": [ { @@ -1480,6 +1570,7 @@ }, { "cell_type": "markdown", + "id": "81d6a9c1", "metadata": {}, "source": [ "You can also pass an optional `file_type` MIME prefix and/or a list of file extensions." @@ -1488,6 +1579,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ca8823d0", "metadata": {}, "outputs": [ { @@ -1513,6 +1605,7 @@ { "cell_type": "code", "execution_count": null, + "id": "73825e11", "metadata": {}, "outputs": [], "source": [ @@ -1526,6 +1619,7 @@ { "cell_type": "code", "execution_count": null, + "id": "695f90e0", "metadata": {}, "outputs": [], "source": [ @@ -1541,6 +1635,7 @@ }, { "cell_type": "markdown", + "id": "6f1433bf", "metadata": {}, "source": [ "fastai also updates the `repr` of `Path` such that, if `Path.BASE_PATH` is defined, all paths are printed relative to that path (as long as they are contained in `Path.BASE_PATH`:" @@ -1549,6 +1644,7 @@ { "cell_type": "code", "execution_count": null, + "id": "bc55f58d", "metadata": {}, "outputs": [], "source": [ @@ -1562,6 +1658,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6bd8b653", "metadata": {}, "outputs": [], "source": [ @@ -1578,6 +1675,7 @@ }, { "cell_type": "markdown", + "id": "f734e90a", "metadata": {}, "source": [ "## Reindexing Collections" @@ -1586,6 +1684,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f2bb033e", "metadata": {}, "outputs": [], "source": [ @@ -1599,6 +1698,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0356640b", "metadata": {}, "outputs": [], "source": [ @@ -1631,6 +1731,7 @@ { "cell_type": "code", "execution_count": null, + "id": "37757e70", "metadata": {}, "outputs": [ { @@ -1669,6 +1770,7 @@ }, { "cell_type": "markdown", + "id": "8e4bd400", "metadata": {}, "source": [ "This is useful when constructing batches or organizing data in a particular manner (i.e. for deep learning). This class is primarly used in organizing data for language models in fastai." @@ -1676,6 +1778,7 @@ }, { "cell_type": "markdown", + "id": "c1fa5ccd", "metadata": {}, "source": [ "You can supply a custom index upon instantiation with the `idxs` argument, or you can call the `reindex` method to supply a new index for your collection.\n", @@ -1686,6 +1789,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8bbb37ef", "metadata": {}, "outputs": [ { @@ -1706,6 +1810,7 @@ }, { "cell_type": "markdown", + "id": "8f8fbf20", "metadata": {}, "source": [ "Alternatively, you can use the `reindex` method:" @@ -1714,6 +1819,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9a4a863c", "metadata": {}, "outputs": [ { @@ -1753,6 +1859,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2a07b2f9", "metadata": {}, "outputs": [ { @@ -1774,6 +1881,7 @@ }, { "cell_type": "markdown", + "id": "cebb8d47", "metadata": {}, "source": [ "You can optionally specify a LRU cache, which uses [functools.lru_cache](https://docs.python.org/3/library/functools.html#functools.lru_cache) upon instantiation:" @@ -1782,6 +1890,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c8e25bc5", "metadata": {}, "outputs": [ { @@ -1806,6 +1915,7 @@ }, { "cell_type": "markdown", + "id": "893bbd1d", "metadata": {}, "source": [ "You can optionally clear the LRU cache by calling the `cache_clear` method:" @@ -1814,6 +1924,7 @@ { "cell_type": "code", "execution_count": null, + "id": "94118043", "metadata": {}, "outputs": [ { @@ -1853,6 +1964,7 @@ { "cell_type": "code", "execution_count": null, + "id": "da0f45a7", "metadata": {}, "outputs": [ { @@ -1879,6 +1991,7 @@ { "cell_type": "code", "execution_count": null, + "id": "2f2c5d80", "metadata": {}, "outputs": [ { @@ -1917,6 +2030,7 @@ }, { "cell_type": "markdown", + "id": "d6358067", "metadata": {}, "source": [ "Note that an ordered index is automatically constructed for the data structure even if one is not supplied." @@ -1925,6 +2039,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7cafcd0c", "metadata": {}, "outputs": [ { @@ -1947,6 +2062,7 @@ { "cell_type": "code", "execution_count": null, + "id": "642c0087", "metadata": {}, "outputs": [], "source": [ @@ -1967,6 +2083,7 @@ { "cell_type": "code", "execution_count": null, + "id": "eecd98f0", "metadata": {}, "outputs": [], "source": [ @@ -1978,6 +2095,7 @@ }, { "cell_type": "markdown", + "id": "f4cdba02", "metadata": {}, "source": [ "## `SaveReturn` and `save_iter` Variants" @@ -1985,6 +2103,7 @@ }, { "cell_type": "markdown", + "id": "f0512f6b", "metadata": {}, "source": [ "These utilities solve a common problem in Python: how to extract additional information from generator functions beyond just the yielded values.\n", @@ -1995,6 +2114,7 @@ { "cell_type": "code", "execution_count": null, + "id": "566d7941", "metadata": {}, "outputs": [], "source": [ @@ -2012,6 +2132,7 @@ { "cell_type": "code", "execution_count": null, + "id": "59a2f54d", "metadata": {}, "outputs": [], "source": [ @@ -2026,6 +2147,7 @@ }, { "cell_type": "markdown", + "id": "a668abc8", "metadata": {}, "source": [ "`SaveReturn` is the simplest approach to solving this problem - it wraps any existing (non-async) generator and captures its return value. This works because `yield from` (used internally in `SaveReturn`) returns the value from the `return` of the generator function." @@ -2034,6 +2156,7 @@ { "cell_type": "code", "execution_count": null, + "id": "cbb65e6f", "metadata": {}, "outputs": [ { @@ -2070,6 +2193,7 @@ }, { "cell_type": "markdown", + "id": "7da2bd54", "metadata": {}, "source": [ "In order to provide an accurate signature for `save_iter`, we need a version of `wraps` that removes leading parameters:" @@ -2078,6 +2202,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e89ff9b0", "metadata": {}, "outputs": [], "source": [ @@ -2096,6 +2221,7 @@ }, { "cell_type": "markdown", + "id": "bb914f42", "metadata": {}, "source": [ "`trim_wraps` is a decorator factory that works like `functools.wraps`, but removes the first `n` parameters from the wrapped function's signature. This is useful when creating wrapper functions that consume some parameters internally and shouldn't expose them in the public API." @@ -2104,6 +2230,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b4d2e6d1", "metadata": {}, "outputs": [ { @@ -2129,6 +2256,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b867efdd", "metadata": {}, "outputs": [], "source": [ @@ -2147,6 +2275,7 @@ }, { "cell_type": "markdown", + "id": "0848fe74", "metadata": {}, "source": [ "`save_iter` modifies generator functions to store state in the iterator object itself. The generator receives an object as its first parameter, which it can use to store attributes. You can store values during iteration, not just at the end,\n", @@ -2156,6 +2285,7 @@ { "cell_type": "code", "execution_count": null, + "id": "55b01b36", "metadata": {}, "outputs": [], "source": [ @@ -2170,6 +2300,7 @@ }, { "cell_type": "markdown", + "id": "d2ec2881", "metadata": {}, "source": [ "Because iternally `save_iter` uses `trim_wraps`, the signature of `sum_range` correctly shows that you should *not* pass `o` to it; it's injected by the decorating function." @@ -2178,6 +2309,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b3c5fb3d", "metadata": {}, "outputs": [ { @@ -2195,6 +2327,7 @@ { "cell_type": "code", "execution_count": null, + "id": "65335317", "metadata": {}, "outputs": [ { @@ -2215,6 +2348,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d4017861", "metadata": {}, "outputs": [], "source": [ @@ -2228,6 +2362,7 @@ }, { "cell_type": "markdown", + "id": "748780cc", "metadata": {}, "source": [ "`asave_iter` provides the same functionality as `save_iter`, but for async generator functions. `yield from` and `return` can not be used with async generator functions, so `SaveReturn` can't be used here." @@ -2236,6 +2371,7 @@ { "cell_type": "code", "execution_count": null, + "id": "41de7df9", "metadata": {}, "outputs": [ { @@ -2263,6 +2399,7 @@ }, { "cell_type": "markdown", + "id": "fcdf7990", "metadata": {}, "source": [ "## Other Helpers" @@ -2271,6 +2408,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4e02b1f0", "metadata": {}, "outputs": [], "source": [ @@ -2289,6 +2427,7 @@ }, { "cell_type": "markdown", + "id": "63af79b7", "metadata": {}, "source": [ "`friendly_name` generates random, human-readable names by combining adjectives, nouns, verbs, and adverbs with a random alphanumeric suffix. This is useful for creating memorable identifiers for temporary files, test data, or user-friendly resource names." @@ -2297,6 +2436,7 @@ { "cell_type": "code", "execution_count": null, + "id": "dc83b044", "metadata": {}, "outputs": [ { @@ -2316,6 +2456,7 @@ }, { "cell_type": "markdown", + "id": "7cbee8ba", "metadata": {}, "source": [ "Names are hyphen-separated and follow the pattern `adjective-noun-verb-adverb`, randomly chosen from lists of size 102, 116, 110, and 30, respectively. The `levels` param selects how many of the names to include:" @@ -2324,6 +2465,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7a518ed2", "metadata": {}, "outputs": [ { @@ -2343,6 +2485,7 @@ }, { "cell_type": "markdown", + "id": "bb5fc91d", "metadata": {}, "source": [ "`suffix` sets the length of the random alphanumeric ending. Each suffix item is taken from the 36 options of lowercase letters plus digits." @@ -2351,6 +2494,7 @@ { "cell_type": "code", "execution_count": null, + "id": "00cea859", "metadata": {}, "outputs": [ { @@ -2371,6 +2515,7 @@ { "cell_type": "code", "execution_count": null, + "id": "998e08a0", "metadata": {}, "outputs": [], "source": [ @@ -2383,6 +2528,7 @@ }, { "cell_type": "markdown", + "id": "14866139", "metadata": {}, "source": [ "The number of combinations if all levels are included is:" @@ -2391,6 +2537,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ae2234cc", "metadata": {}, "outputs": [ { @@ -2407,6 +2554,7 @@ }, { "cell_type": "markdown", + "id": "08a1d52e", "metadata": {}, "source": [ "The default settings give:" @@ -2415,6 +2563,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6419cd57", "metadata": {}, "outputs": [ { @@ -2432,6 +2581,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b4d1831f", "metadata": {}, "outputs": [], "source": [ @@ -2457,6 +2607,7 @@ }, { "cell_type": "markdown", + "id": "fe6cfd8b", "metadata": {}, "source": [ "This is a combination of `eval` and `exec`, which behaves like ipython and Jupyter. If the last line is an expression, it is evaluated and the result is returned:" @@ -2465,6 +2616,7 @@ { "cell_type": "code", "execution_count": null, + "id": "155b5188", "metadata": {}, "outputs": [ { @@ -2487,6 +2639,7 @@ }, { "cell_type": "markdown", + "id": "303ea074", "metadata": {}, "source": [ "By default, the code uses the caller's globals and locals. For instance, here `f` is available since it's been added to our symbol table:" @@ -2495,6 +2648,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5a7f35b1", "metadata": {}, "outputs": [ { @@ -2511,6 +2665,7 @@ }, { "cell_type": "markdown", + "id": "38ab04db", "metadata": {}, "source": [ "Pass a dict as the `g` param in order to use an arbitrary namespace:" @@ -2519,6 +2674,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ea8d90cb", "metadata": {}, "outputs": [ { @@ -2536,6 +2692,7 @@ { "cell_type": "code", "execution_count": null, + "id": "bdaa78a0", "metadata": {}, "outputs": [], "source": [ @@ -2547,6 +2704,7 @@ }, { "cell_type": "markdown", + "id": "5a33f602", "metadata": {}, "source": [ "This function helps us identify the first declared raw function of a dispatched function:" @@ -2555,6 +2713,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f6f873e2", "metadata": {}, "outputs": [], "source": [ @@ -2564,6 +2723,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6443f18b", "metadata": {}, "outputs": [], "source": [ @@ -2578,6 +2738,7 @@ { "cell_type": "code", "execution_count": null, + "id": "bb4d3fe2", "metadata": {}, "outputs": [], "source": [ @@ -2608,6 +2769,7 @@ }, { "cell_type": "markdown", + "id": "0553d4a3", "metadata": {}, "source": [ "`get_source_link` allows you get a link to source code related to an object. For [nbdev](https://github.com/fastai/nbdev) related projects such as fastcore, we can get the full link to a GitHub repo. For `nbdev` projects, be sure to properly set the `git_url` in `settings.ini` (derived from `lib_name` and `branch` on top of the prefix you will need to adapt) so that those links are correct.\n", @@ -2618,6 +2780,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7cdda557", "metadata": {}, "outputs": [], "source": [ @@ -2627,6 +2790,7 @@ { "cell_type": "code", "execution_count": null, + "id": "cf6b05e3", "metadata": {}, "outputs": [ { @@ -2649,6 +2813,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f385fa0d", "metadata": {}, "outputs": [], "source": [ @@ -2661,6 +2826,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b90bf27d", "metadata": {}, "outputs": [], "source": [ @@ -2676,6 +2842,7 @@ { "cell_type": "code", "execution_count": null, + "id": "31a380eb", "metadata": {}, "outputs": [], "source": [ @@ -2686,6 +2853,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a6371687", "metadata": {}, "outputs": [], "source": [ @@ -2702,6 +2870,7 @@ { "cell_type": "code", "execution_count": null, + "id": "189818ca", "metadata": {}, "outputs": [], "source": [ @@ -2718,6 +2887,7 @@ { "cell_type": "code", "execution_count": null, + "id": "db3588fa", "metadata": {}, "outputs": [ { @@ -2737,6 +2907,7 @@ }, { "cell_type": "markdown", + "id": "4a8b2dc6", "metadata": {}, "source": [ "You can set a maximum and minimum for the y-axis of the sparkline with the arguments `mn` and `mx` respectively:" @@ -2745,6 +2916,7 @@ { "cell_type": "code", "execution_count": null, + "id": "cc1f2daf", "metadata": {}, "outputs": [ { @@ -2765,6 +2937,7 @@ { "cell_type": "code", "execution_count": null, + "id": "81d193dd", "metadata": {}, "outputs": [], "source": [ @@ -2782,6 +2955,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0356819b", "metadata": {}, "outputs": [], "source": [ @@ -2796,6 +2970,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6eed0273", "metadata": {}, "outputs": [], "source": [ @@ -2810,6 +2985,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5f2a4659", "metadata": {}, "outputs": [], "source": [ @@ -2824,6 +3000,7 @@ { "cell_type": "code", "execution_count": null, + "id": "aa966f19", "metadata": {}, "outputs": [], "source": [ @@ -2841,6 +3018,7 @@ }, { "cell_type": "markdown", + "id": "9a8484e4", "metadata": {}, "source": [ "This sets the number of threads consistently for many tools, by:\n", @@ -2852,6 +3030,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4dac8b41", "metadata": {}, "outputs": [], "source": [ @@ -2866,6 +3045,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a0f6eaa4", "metadata": {}, "outputs": [], "source": [ @@ -2880,6 +3060,7 @@ { "cell_type": "code", "execution_count": null, + "id": "205987ad", "metadata": {}, "outputs": [], "source": [ @@ -2897,6 +3078,7 @@ { "cell_type": "code", "execution_count": null, + "id": "713d7560", "metadata": {}, "outputs": [], "source": [ @@ -2928,6 +3110,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9420f9d8", "metadata": {}, "outputs": [ { @@ -2966,6 +3149,7 @@ }, { "cell_type": "markdown", + "id": "66976ac1", "metadata": {}, "source": [ "Add events with `add`, and get number of `events` and their frequency (`freq`)." @@ -2974,6 +3158,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d5d39cff", "metadata": {}, "outputs": [ { @@ -2998,6 +3183,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a79aa5d5", "metadata": {}, "outputs": [], "source": [ @@ -3008,6 +3194,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b5bc6dd4", "metadata": {}, "outputs": [], "source": [ @@ -3020,6 +3207,7 @@ { "cell_type": "code", "execution_count": null, + "id": "10efda65", "metadata": {}, "outputs": [], "source": [ @@ -3030,6 +3218,7 @@ { "cell_type": "code", "execution_count": null, + "id": "14ea0571", "metadata": {}, "outputs": [], "source": [ @@ -3053,6 +3242,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d1bababa", "metadata": {}, "outputs": [ { @@ -3092,6 +3282,7 @@ { "cell_type": "code", "execution_count": null, + "id": "eb9cb738", "metadata": {}, "outputs": [], "source": [ @@ -3105,6 +3296,7 @@ }, { "cell_type": "markdown", + "id": "24bccd15", "metadata": {}, "source": [ "The result is a tuple of `(formatted_string,missing_fields,extra_fields)`, e.g:" @@ -3113,6 +3305,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c990509b", "metadata": {}, "outputs": [], "source": [ @@ -3125,6 +3318,7 @@ { "cell_type": "code", "execution_count": null, + "id": "32fa9ba8", "metadata": {}, "outputs": [], "source": [ @@ -3137,6 +3331,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d1278d6c", "metadata": {}, "outputs": [ { @@ -3155,6 +3350,7 @@ { "cell_type": "code", "execution_count": null, + "id": "55abbab7", "metadata": {}, "outputs": [], "source": [ @@ -3167,6 +3363,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8756e202", "metadata": {}, "outputs": [ { @@ -3184,6 +3381,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f32ab7a7", "metadata": {}, "outputs": [], "source": [ @@ -3201,6 +3399,7 @@ }, { "cell_type": "markdown", + "id": "7ed2078d", "metadata": {}, "source": [ "You can add a breakpoint to an existing function, e.g:\n", @@ -3216,6 +3415,7 @@ { "cell_type": "code", "execution_count": null, + "id": "45f779a3", "metadata": {}, "outputs": [], "source": [ @@ -3236,6 +3436,7 @@ { "cell_type": "code", "execution_count": null, + "id": "3d0d0d9a", "metadata": {}, "outputs": [], "source": [ @@ -3255,6 +3456,7 @@ { "cell_type": "code", "execution_count": null, + "id": "12f93aed", "metadata": {}, "outputs": [], "source": [ @@ -3269,6 +3471,7 @@ { "cell_type": "code", "execution_count": null, + "id": "423b8c58", "metadata": {}, "outputs": [ { @@ -3308,6 +3511,7 @@ { "cell_type": "code", "execution_count": null, + "id": "f58a49b6", "metadata": {}, "outputs": [], "source": [ @@ -3322,6 +3526,7 @@ { "cell_type": "code", "execution_count": null, + "id": "705c1a5a", "metadata": {}, "outputs": [], "source": [ @@ -3340,6 +3545,7 @@ { "cell_type": "code", "execution_count": null, + "id": "01a02b78", "metadata": {}, "outputs": [], "source": [ @@ -3356,6 +3562,7 @@ }, { "cell_type": "markdown", + "id": "f3371296", "metadata": {}, "source": [ "When we display code in a notebook, it's nice to highlight it, so we create a function to simplify that:" @@ -3364,6 +3571,7 @@ { "cell_type": "code", "execution_count": null, + "id": "abf99c3f", "metadata": {}, "outputs": [ { @@ -3389,6 +3597,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b59609d4", "metadata": {}, "outputs": [], "source": [ @@ -3407,6 +3616,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b8b7b6c4", "metadata": {}, "outputs": [], "source": [ @@ -3416,6 +3626,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9ef13805", "metadata": {}, "outputs": [], "source": [ @@ -3431,6 +3642,7 @@ { "cell_type": "code", "execution_count": null, + "id": "58908987", "metadata": {}, "outputs": [ { @@ -3454,6 +3666,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ecd7f0a2", "metadata": {}, "outputs": [], "source": [ @@ -3469,6 +3682,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1e9a46ab", "metadata": {}, "outputs": [], "source": [ @@ -3483,6 +3697,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c7d70e0c", "metadata": {}, "outputs": [ { @@ -3505,6 +3720,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e62e3c77", "metadata": {}, "outputs": [], "source": [ @@ -3534,6 +3750,7 @@ { "cell_type": "code", "execution_count": null, + "id": "72b22fa8", "metadata": {}, "outputs": [ { @@ -3558,6 +3775,7 @@ { "cell_type": "code", "execution_count": null, + "id": "19516b10", "metadata": {}, "outputs": [ { @@ -3578,6 +3796,7 @@ { "cell_type": "code", "execution_count": null, + "id": "bef2318a", "metadata": {}, "outputs": [ { @@ -3598,6 +3817,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9fa70a25", "metadata": {}, "outputs": [], "source": [ @@ -3615,6 +3835,7 @@ }, { "cell_type": "markdown", + "id": "c260193e", "metadata": {}, "source": [ "This can be used as a decorator..." @@ -3623,6 +3844,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9fd419f3", "metadata": {}, "outputs": [ { @@ -3646,6 +3868,7 @@ }, { "cell_type": "markdown", + "id": "08b2e099", "metadata": {}, "source": [ "...or can update the behavior of an existing class (or dataclass):" @@ -3654,6 +3877,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5f74eaf5", "metadata": {}, "outputs": [ { @@ -3677,6 +3901,7 @@ }, { "cell_type": "markdown", + "id": "362e82a0", "metadata": {}, "source": [ "Action occurs in-place:" @@ -3685,6 +3910,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0a22024f", "metadata": {}, "outputs": [ { @@ -3708,6 +3934,7 @@ { "cell_type": "code", "execution_count": null, + "id": "7729d860", "metadata": {}, "outputs": [], "source": [ @@ -3728,6 +3955,7 @@ }, { "cell_type": "markdown", + "id": "cf354d0f", "metadata": {}, "source": [ "Any `UNSET` values are not included." @@ -3736,6 +3964,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4056c9d1", "metadata": {}, "outputs": [ { @@ -3755,6 +3984,7 @@ }, { "cell_type": "markdown", + "id": "326a1eac", "metadata": {}, "source": [ "Set the optional `__flds__` parameter to customise the field list, and the optional `__skip__` parameter to skip some names." @@ -3763,6 +3993,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4281b968", "metadata": {}, "outputs": [], "source": [ @@ -3777,6 +4008,7 @@ }, { "cell_type": "markdown", + "id": "efba9860", "metadata": {}, "source": [ "To customise dict conversion behavior for a class, implement the `_asdict` method (this is used in the Python stdlib for named tuples)." @@ -3785,6 +4017,7 @@ { "cell_type": "code", "execution_count": null, + "id": "674a899f", "metadata": {}, "outputs": [], "source": [ @@ -3797,6 +4030,7 @@ }, { "cell_type": "markdown", + "id": "56229257", "metadata": {}, "source": [ "The `vars_pub` function returns a list of public (non-underscore-prefixed) variable names from an object, excluding any names listed in the object's optional `__skip__` attribute." @@ -3805,6 +4039,7 @@ { "cell_type": "code", "execution_count": null, + "id": "177d649d", "metadata": {}, "outputs": [], "source": [ @@ -3818,6 +4053,7 @@ }, { "cell_type": "markdown", + "id": "ed8e6e2c", "metadata": {}, "source": [ "Without `__skip__`, all pub vars are returned" @@ -3826,6 +4062,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a858508f", "metadata": {}, "outputs": [], "source": [ @@ -3839,6 +4076,7 @@ { "cell_type": "code", "execution_count": null, + "id": "575add44", "metadata": {}, "outputs": [], "source": [ @@ -3852,6 +4090,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a94bb58f", "metadata": {}, "outputs": [], "source": [ @@ -3864,6 +4103,7 @@ { "cell_type": "code", "execution_count": null, + "id": "557c2708", "metadata": {}, "outputs": [], "source": [ @@ -3876,6 +4116,7 @@ { "cell_type": "code", "execution_count": null, + "id": "de67dbf3", "metadata": {}, "outputs": [], "source": [ @@ -3886,6 +4127,7 @@ { "cell_type": "code", "execution_count": null, + "id": "bb02cce3", "metadata": {}, "outputs": [], "source": [ @@ -3901,6 +4143,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1fdc8e07", "metadata": {}, "outputs": [ { @@ -3934,6 +4177,7 @@ { "cell_type": "code", "execution_count": null, + "id": "64471925", "metadata": {}, "outputs": [], "source": [ @@ -3949,6 +4193,7 @@ { "cell_type": "code", "execution_count": null, + "id": "86cb7a86", "metadata": {}, "outputs": [], "source": [ @@ -3962,6 +4207,7 @@ }, { "cell_type": "markdown", + "id": "d33cb29d", "metadata": {}, "source": [ "`CachedCoro` and `reawaitable` are partly based on [python issue tracker](https://bugs.python.org/issue46622) code from Serhiy Storchaka. They allow an awaitable to be called multiple times." @@ -3970,6 +4216,7 @@ { "cell_type": "code", "execution_count": null, + "id": "a7336c4d", "metadata": {}, "outputs": [ { @@ -3995,6 +4242,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d2b4fe09", "metadata": {}, "outputs": [], "source": [ @@ -4035,6 +4283,7 @@ }, { "cell_type": "markdown", + "id": "eb9c20e4", "metadata": {}, "source": [ "This is a flexible lru cache function that you can pass a list of functions to. Those functions define the cache eviction policy. For instance, `time_policy` is provided for time-based cache eviction, and `mtime_policy` evicts based on a file's modified-time changing. The policy functions are passed the last value that function returned was (initially `None`), and return a new value to indicate the cache has expired. When the cache expires, all functions are called with `None` to force getting new values." @@ -4043,6 +4292,7 @@ { "cell_type": "code", "execution_count": null, + "id": "cf131274", "metadata": {}, "outputs": [], "source": [ @@ -4058,6 +4308,7 @@ { "cell_type": "code", "execution_count": null, + "id": "8387a8bf", "metadata": {}, "outputs": [], "source": [ @@ -4073,6 +4324,7 @@ { "cell_type": "code", "execution_count": null, + "id": "e16e7b41", "metadata": {}, "outputs": [ { @@ -4096,6 +4348,7 @@ { "cell_type": "code", "execution_count": null, + "id": "62e3eef3", "metadata": {}, "outputs": [ { @@ -4127,6 +4380,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1094d811", "metadata": {}, "outputs": [], "source": [ @@ -4139,6 +4393,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4623e407", "metadata": {}, "outputs": [], "source": [ @@ -4164,6 +4419,7 @@ }, { "cell_type": "markdown", + "id": "1f7b1b37", "metadata": {}, "source": [ "This function is a small convenience wrapper for using `flexicache` with `time_policy`." @@ -4172,6 +4428,7 @@ { "cell_type": "code", "execution_count": null, + "id": "0b594c8f", "metadata": {}, "outputs": [], "source": [ @@ -4205,6 +4462,7 @@ }, { "cell_type": "markdown", + "id": "9af115f8", "metadata": {}, "source": [ "# Export -" @@ -4213,6 +4471,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1081350e", "metadata": {}, "outputs": [], "source": [ @@ -4223,21 +4482,13 @@ { "cell_type": "code", "execution_count": null, + "id": "90153c73", "metadata": {}, "outputs": [], "source": [] } ], - "metadata": { - "jupytext": { - "split_at_heading": true - }, - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, + "metadata": {}, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index 0f66cdfa..c3de3139 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -744,13 +744,7 @@ "source": [] } ], - "metadata": { - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } From 953d2af3a59aeef04bc37f4587a698dd948706fd Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 11:35:43 +1000 Subject: [PATCH 158/182] fixes #714 --- fastcore/foundation.py | 12 +- nbs/02_foundation.ipynb | 1257 ++++++++++++++++++++++++++++++++++++--- nbs/03_xtras.ipynb | 354 ++++++++--- 3 files changed, 1442 insertions(+), 181 deletions(-) diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 71a17c83..5acc288b 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -307,12 +307,6 @@ def copy(self:L): "Same as `list.copy`, but returns an `L`" return self._new(self.items.copy()) -# %% ../nbs/02_foundation.ipynb -@patch -def cycle(self:L): - "Same as `itertools.cycle`" - return cycle(self) - # %% ../nbs/02_foundation.ipynb @patch def shuffle(self:L): @@ -351,6 +345,12 @@ def setattrs(self:L, attr, val): "Call `setattr` on all items" [setattr(o,attr,val) for o in self] +# %% ../nbs/02_foundation.ipynb +@patch +def cycle(self:L): + "Same as `itertools.cycle`" + return cycle(self) + # %% ../nbs/02_foundation.ipynb def save_config_file(file, d, **kwargs): "Write settings dict to a new config file, or overwrite the existing one." diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index b7c26a87..94c6ebe8 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -53,7 +53,9 @@ { "cell_type": "markdown", "id": "13ac17ff", - "metadata": {}, + "metadata": { + "heading_collapsed": true + }, "source": [ "## Foundational Functions" ] @@ -62,7 +64,9 @@ "cell_type": "code", "execution_count": null, "id": "f059df0f", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -79,7 +83,9 @@ "cell_type": "code", "execution_count": null, "id": "c3fd027f", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -100,7 +106,9 @@ { "cell_type": "markdown", "id": "620dc00e", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "`add_docs` allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in [our style guide](https://docs.fast.ai/dev/style.html).\n", "\n", @@ -111,7 +119,9 @@ "cell_type": "code", "execution_count": null, "id": "0832c567", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "class T:\n", @@ -122,7 +132,9 @@ { "cell_type": "markdown", "id": "adedd47e", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "You can add documentation to this class like so:" ] @@ -131,7 +143,9 @@ "cell_type": "code", "execution_count": null, "id": "da9d95f9", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "add_docs(T, cls_doc=\"A docstring for the class.\",\n", @@ -142,7 +156,9 @@ { "cell_type": "markdown", "id": "a91ee046", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "Now, docstrings will appear as expected:" ] @@ -151,7 +167,9 @@ "cell_type": "code", "execution_count": null, "id": "2c4a26aa", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(T.__doc__, \"A docstring for the class.\")\n", @@ -162,7 +180,9 @@ { "cell_type": "markdown", "id": "47545904", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "`add_docs` also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:" ] @@ -171,7 +191,9 @@ "cell_type": "code", "execution_count": null, "id": "a9f77cf6", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "class T:\n", @@ -186,7 +208,9 @@ "cell_type": "code", "execution_count": null, "id": "88805b49", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| hide\n", @@ -205,7 +229,9 @@ "cell_type": "code", "execution_count": null, "id": "b180313a", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -218,7 +244,9 @@ { "cell_type": "markdown", "id": "e25bd40f", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "Instead of using `add_docs`, you can use the decorator `docs` as shown below. Note that the docstring for the class can be set with the argument `cls_doc`:" ] @@ -227,7 +255,9 @@ "cell_type": "code", "execution_count": null, "id": "d10f59f9", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "@docs\n", @@ -248,7 +278,9 @@ { "cell_type": "markdown", "id": "2ad622b1", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "For either the `docs` decorator or the `add_docs` function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the `_docs` attribute:" ] @@ -257,7 +289,9 @@ "cell_type": "code", "execution_count": null, "id": "e4bbd1b9", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "@docs\n", @@ -275,7 +309,9 @@ "cell_type": "code", "execution_count": null, "id": "0b1f9829", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -307,7 +343,9 @@ "cell_type": "code", "execution_count": null, "id": "4e191b5b", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "assert is_iter([1])\n", @@ -320,7 +358,9 @@ "cell_type": "code", "execution_count": null, "id": "34a29b43", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -333,7 +373,9 @@ { "cell_type": "markdown", "id": "198216f8", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "`coll_repr` is used to provide a more informative [`__repr__`](https://stackoverflow.com/questions/1984162/purpose-of-pythons-repr) about list-like objects. `coll_repr` and is used by `L` to build a `__repr__` that displays the length of a list in addition to a preview of a list.\n", "\n", @@ -344,7 +386,9 @@ "cell_type": "code", "execution_count": null, "id": "fe3ed954", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(coll_repr(range(1000),10), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')\n", @@ -357,7 +401,9 @@ "cell_type": "code", "execution_count": null, "id": "6cf96f49", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -370,7 +416,9 @@ "cell_type": "code", "execution_count": null, "id": "66a28a53", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -389,7 +437,9 @@ "cell_type": "code", "execution_count": null, "id": "3b7dd826", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(mask2idxs([False,True,False,True]), [1,3])\n", @@ -401,7 +451,9 @@ "cell_type": "code", "execution_count": null, "id": "af754019", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -415,7 +467,9 @@ "cell_type": "code", "execution_count": null, "id": "7f8805d7", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])\n", @@ -428,7 +482,9 @@ "cell_type": "code", "execution_count": null, "id": "9346b91a", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -441,7 +497,9 @@ "cell_type": "code", "execution_count": null, "id": "21c554a1", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])" @@ -451,7 +509,9 @@ "cell_type": "code", "execution_count": null, "id": "8a281173", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -463,7 +523,9 @@ { "cell_type": "markdown", "id": "2c4497d9", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "You can, for example index a single item in a list with an integer or a 0-dimensional numpy array:" ] @@ -472,7 +534,9 @@ "cell_type": "code", "execution_count": null, "id": "cf94cb54", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "assert is_indexer(1)\n", @@ -482,7 +546,9 @@ { "cell_type": "markdown", "id": "504d89e8", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "However, you cannot index into single item in a list with another list or a numpy array with ndim > 0. " ] @@ -491,7 +557,9 @@ "cell_type": "code", "execution_count": null, "id": "1159335e", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "assert not is_indexer([1, 2])\n", @@ -502,7 +570,9 @@ "cell_type": "code", "execution_count": null, "id": "3fd3133f", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -515,7 +585,9 @@ "cell_type": "code", "execution_count": null, "id": "73a52d33", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -536,7 +608,9 @@ "cell_type": "code", "execution_count": null, "id": "fa5990de", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -557,7 +631,9 @@ "cell_type": "code", "execution_count": null, "id": "6f9ef4e6", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -577,7 +653,9 @@ { "cell_type": "markdown", "id": "d8a026cf", - "metadata": {}, + "metadata": { + "heading_collapsed": true + }, "source": [ "## `L` helpers" ] @@ -586,7 +664,9 @@ "cell_type": "code", "execution_count": null, "id": "eefd2763", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -604,7 +684,9 @@ { "cell_type": "markdown", "id": "0ec47e38", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "`ColBase` is a base class that emulates the functionality of a python `list`:" ] @@ -613,7 +695,9 @@ "cell_type": "code", "execution_count": null, "id": "896956c4", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "class _T(CollBase): pass\n", @@ -1315,6 +1399,74 @@ "test_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11])" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "085b690b", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def starfilter(self:L, f, negate=False, **kwargs):\n", + " \"Like `filter`, but unpacks elements as args to `f`\"\n", + " _f = lambda x: f(*x, **kwargs)\n", + " if negate: _f = not_(_f)\n", + " return self._new(filter(_f, self))" + ] + }, + { + "cell_type": "markdown", + "id": "b4a4837f", + "metadata": {}, + "source": [ + "`L.starfilter` is like `filter`, but unpacks tuple elements as arguments to the predicate:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb878281", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((1,2),(3,1),(2,3)).starfilter(lt), [(1,2),(2,3)])\n", + "test_eq(L((1,2),(3,1),(2,3)).starfilter(lt, negate=True), [(3,1)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0fa80df", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def rstarfilter(self:L, f, negate=False, **kwargs):\n", + " \"Like `starfilter`, but reverse the order of args\"\n", + " _f = lambda x: f(*x[::-1], **kwargs)\n", + " if negate: _f = not_(_f)\n", + " return self._new(filter(_f, self))" + ] + }, + { + "cell_type": "markdown", + "id": "6392c73c", + "metadata": {}, + "source": [ + "`L.rstarfilter` is like `starfilter`, but reverses the order of unpacked arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a26c1ac4", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt), [(2,1),(3,2)]) # 1<2, 3<1 fails, 2<3\n", + "test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt, negate=True), [(1,3)])" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1364,6 +1516,74 @@ "test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6])" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf22acd4", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def starargwhere(self:L, f, negate=False):\n", + " \"Like `argwhere`, but unpacks elements as args to `f`\"\n", + " _f = lambda x: f(*x)\n", + " if negate: _f = not_(_f)\n", + " return self._new(i for i,o in enumerate(self) if _f(o))" + ] + }, + { + "cell_type": "markdown", + "id": "86913776", + "metadata": {}, + "source": [ + "`L.starargwhere` is like `argwhere`, but unpacks tuple elements as arguments to the predicate:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4766805", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt), [0,2])\n", + "test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt, negate=True), [1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f010f9c", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def rstarargwhere(self:L, f, negate=False):\n", + " \"Like `starargwhere`, but reverse the order of args\"\n", + " _f = lambda x: f(*x[::-1])\n", + " if negate: _f = not_(_f)\n", + " return self._new(i for i,o in enumerate(self) if _f(o))" + ] + }, + { + "cell_type": "markdown", + "id": "049e0c39", + "metadata": {}, + "source": [ + "`L.rstarargwhere` is like `starargwhere`, but reverses the order of unpacked arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "155730fd", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt), [0,2]) # 1<2, 3<1 fails, 2<3\n", + "test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt, negate=True), [1])" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1438,6 +1658,74 @@ "test_eq(t.argfirst(lambda o:o>4,negate=True),0)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "90509c05", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def starargfirst(self:L, f, negate=False):\n", + " \"Like `argfirst`, but unpacks elements as args to `f`\"\n", + " _f = lambda x: f(*x)\n", + " if negate: _f = not_(_f)\n", + " return first(i for i,o in self.enumerate() if _f(o))" + ] + }, + { + "cell_type": "markdown", + "id": "8ff3d6cc", + "metadata": {}, + "source": [ + "`L.starargfirst` is like `argfirst`, but unpacks tuple elements as arguments to the predicate:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4044d5c", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((3,1),(1,2),(2,3)).starargfirst(lt), 1)\n", + "test_eq(L((1,2),(3,1),(2,3)).starargfirst(lt, negate=True), 1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc43b9b9", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def rstarargfirst(self:L, f, negate=False):\n", + " \"Like `starargfirst`, but reverse the order of args\"\n", + " _f = lambda x: f(*x[::-1])\n", + " if negate: _f = not_(_f)\n", + " return first(i for i,o in self.enumerate() if _f(o))" + ] + }, + { + "cell_type": "markdown", + "id": "3aa280a5", + "metadata": {}, + "source": [ + "`L.rstarargfirst` is like `starargfirst`, but reverses the order of unpacked arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8dd7abc2", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((1,3),(2,1),(3,2)).rstarargfirst(lt), 1) # 3<1 fails, 1<2\n", + "test_eq(L((2,1),(1,3),(3,2)).rstarargfirst(lt, negate=True), 1)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1550,6 +1838,38 @@ "test_eq(L([(1,2,3),(4,5,6)]).starmap(lambda a,b,c: a+b*c), [7,34])" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddc5b018", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def rstarmap(self:L, f, *args, **kwargs):\n", + " \"Like `starmap`, but reverse the order of args\"\n", + " return self._new(itertools.starmap(lambda *x: f(*x[::-1], *args, **kwargs), self))" + ] + }, + { + "cell_type": "markdown", + "id": "cd0db578", + "metadata": {}, + "source": [ + "`L.rstarmap` is like `starmap`, but reverses the order of unpacked arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe98f512", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((1,2),(3,4)).rstarmap(operator.sub), [1,1]) # 2-1, 4-3\n", + "test_eq(L(('a','b'),('c','d')).rstarmap('{}{}'.format), ['ba','dc'])" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1770,112 +2090,144 @@ { "cell_type": "code", "execution_count": null, - "id": "9d86b896", + "id": "1c21919b", "metadata": {}, "outputs": [], "source": [ - "#| export\n", "@patch\n", - "def concat(self:L):\n", - " \"Concatenate all elements of list\"\n", - " return self._new(itertools.chain.from_iterable(self.map(L)))" + "def starsorted(self:L, key, reverse=False):\n", + " \"Like `sorted`, but unpacks elements as args to `key`\"\n", + " return self._new(sorted(self, key=lambda x: key(*x), reverse=reverse))" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "6c74fb84", - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b493aad4", + "cell_type": "markdown", + "id": "7d37f257", "metadata": {}, - "outputs": [], "source": [ - "#| export\n", - "@patch\n", - "def copy(self:L):\n", - " \"Same as `list.copy`, but returns an `L`\"\n", - " return self._new(self.items.copy())" + "`L.starsorted` is like `sorted`, but unpacks tuple elements as arguments to the key function:" ] }, { "cell_type": "code", "execution_count": null, - "id": "b4aaf8fa", + "id": "74a73a56", "metadata": {}, "outputs": [], "source": [ - "t = L([0,1,2,3],4,L(5,6)).copy()\n", - "test_eq(t.concat(), range(7))" + "test_eq(L((3,1),(1,2),(2,0)).starsorted(operator.sub), [(1,2),(3,1),(2,0)]) # sorted by a-b: 2, 2, -1\n", + "test_eq(L((1,2),(3,1),(2,3)).starsorted(operator.add), [(1,2),(3,1),(2,3)]) # sorted by a+b: 3, 4, 5" ] }, { "cell_type": "code", "execution_count": null, - "id": "75215b78", + "id": "7549144e", "metadata": {}, "outputs": [], "source": [ - "#| export\n", "@patch\n", - "def cycle(self:L):\n", - " \"Same as `itertools.cycle`\"\n", - " return cycle(self)" + "def rstarsorted(self:L, key, reverse=False):\n", + " \"Like `starsorted`, but reverse the order of args\"\n", + " return self._new(sorted(self, key=lambda x: key(*x[::-1]), reverse=reverse))" ] }, { "cell_type": "markdown", - "id": "275a5e35", + "id": "4bd69264", "metadata": {}, "source": [ - "`L.cycle` returns an infinite iterator that cycles through the elements:" + "`L.rstarsorted` is like `starsorted`, but reverses the order of unpacked arguments:" ] }, { "cell_type": "code", "execution_count": null, - "id": "a34356a8", + "id": "7123a1a2", "metadata": {}, "outputs": [], "source": [ - "test_eq(list(itertools.islice(L(1,2,3).cycle(), 7)), [1,2,3,1,2,3,1])" + "test_eq(L((1,3),(2,1),(0,2)).rstarsorted(operator.sub), [(2,1),(1,3),(0,2)]) # sorted by b-a: 0, 2, 2\n", + "test_eq(L((2,1),(1,3),(3,2)).rstarsorted(operator.sub), [(2,1),(3,2),(1,3)]) # sorted by b-a: -1, -1, 2" ] }, { "cell_type": "code", "execution_count": null, - "id": "7b2691d6", + "id": "9d86b896", "metadata": {}, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def shuffle(self:L):\n", - " \"Same as `random.shuffle`, but not inplace\"\n", - " it = copy(self.items)\n", - " random.shuffle(it)\n", - " return self._new(it)" + "def concat(self:L):\n", + " \"Concatenate all elements of list\"\n", + " return self._new(itertools.chain.from_iterable(self.map(L)))" ] }, { - "cell_type": "markdown", - "id": "30e80f78", + "cell_type": "code", + "execution_count": null, + "id": "6c74fb84", "metadata": {}, + "outputs": [], "source": [ - "`L.shuffle` returns a new shuffled `L`, leaving the original unchanged:" + "test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))" ] }, { "cell_type": "code", "execution_count": null, - "id": "3edd22b5", + "id": "b493aad4", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def copy(self:L):\n", + " \"Same as `list.copy`, but returns an `L`\"\n", + " return self._new(self.items.copy())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4aaf8fa", + "metadata": {}, + "outputs": [], + "source": [ + "t = L([0,1,2,3],4,L(5,6)).copy()\n", + "test_eq(t.concat(), range(7))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b2691d6", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def shuffle(self:L):\n", + " \"Same as `random.shuffle`, but not inplace\"\n", + " it = copy(self.items)\n", + " random.shuffle(it)\n", + " return self._new(it)" + ] + }, + { + "cell_type": "markdown", + "id": "30e80f78", + "metadata": {}, + "source": [ + "`L.shuffle` returns a new shuffled `L`, leaving the original unchanged:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3edd22b5", "metadata": {}, "outputs": [], "source": [ @@ -1910,6 +2262,91 @@ "test_eq(L(1,2,3,4).reduce(operator.mul, 10), 240)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "3db27a6a", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def starreduce(self:L, f, initial=None):\n", + " \"Like `reduce`, but unpacks elements as args to `f`\"\n", + " _f = lambda acc, x: f(acc, *x)\n", + " return reduce(_f, self) if initial is None else reduce(_f, self, initial)" + ] + }, + { + "cell_type": "markdown", + "id": "9f4bd61b", + "metadata": {}, + "source": [ + "`L.starreduce` is like `reduce`, but unpacks tuple elements as additional arguments to `f` (after accumulator):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41fd24f2", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((1,2),(3,4),(5,6)).starreduce(lambda acc,a,b: acc+a*b, 0), 44) # 0+1*2+3*4+5*6\n", + "test_eq(L(('a',1),('b',2)).starreduce(lambda acc,k,v: {**acc, k:v}, {}), {'a':1,'b':2})" + ] + }, + { + "cell_type": "markdown", + "id": "cc8968f6", + "metadata": {}, + "source": [ + "E.g implement a dot product:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dfe97eec", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "44" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def dot(a,b): return a.zipwith(b).starreduce(lambda acc,a,b: acc+a*b, 0)\n", + "dot(L(1,3,5), L(2,4,6))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "046f01fb", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def rstarreduce(self:L, f, initial=None):\n", + " \"Like `starreduce`, but reverse the order of unpacked args\"\n", + " _f = lambda acc, x: f(acc, *x[::-1])\n", + " return reduce(_f, self) if initial is None else reduce(_f, self, initial)" + ] + }, + { + "cell_type": "markdown", + "id": "37c7530a", + "metadata": {}, + "source": [ + "`L.rstarreduce` is like `starreduce`, but reverses the order of unpacked arguments:" + ] + }, { "cell_type": "code", "execution_count": null, @@ -2011,6 +2448,645 @@ "test_eq(t.attrgot('foo'), ['bar','bar'])" ] }, + { + "cell_type": "markdown", + "id": "d0e47eb0", + "metadata": {}, + "source": [ + "### itertools wrappers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7179fe6", + "metadata": { + "time_run": "2025-12-13T01:35:17.762331+00:00" + }, + "outputs": [], + "source": [ + "from dialoghelper import *\n", + "dh_settings['port']=6001" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "daf6d8e4", + "metadata": {}, + "outputs": [], + "source": [ + "# tool_info()" + ] + }, + { + "cell_type": "markdown", + "id": "c96ed60e", + "metadata": {}, + "source": [ + "Tools available from `dialoghelper`:\n", + "\n", + "- &`curr_dialog`: Get the current dialog info.\n", + "- &`msg_idx`: Get absolute index of message in dialog.\n", + "- &`add_html`: Send HTML to the browser to be swapped into the DOM using hx-swap-oob.\n", + "- &`find_msg_id`: Get the current message id.\n", + "- &`find_msgs`: Find messages in current specific dialog that contain the given information.\n", + " - (solveit can often get this id directly from its context, and will not need to use this if the required information is already available to it.)\n", + "- &`read_msg`: Get the message indexed in the current dialog.\n", + " - To get the exact message use `n=0` and `relative=True` together with `msgid`.\n", + " - To get a relative message use `n` (relative position index).\n", + " - To get the nth message use `n` with `relative=False`, e.g `n=0` first message, `n=-1` last message.\n", + "- &`del_msg`: Delete a message from the dialog.\n", + "- &`add_msg`: Add/update a message to the queue to show after code execution completes.\n", + "- &`update_msg`: Update an existing message.\n", + "- &`url2note`: Read URL as markdown, and add a note below current message with the result\n", + "- &`msg_insert_line`: Insert text at a specific location in a message.\n", + "- &`msg_str_replace`: Find and replace text in a message.\n", + "- &`msg_strs_replace`: Find and replace multiple strings in a message.\n", + "- &`msg_replace_lines`: Replace a range of lines in a message with new content.\n", + " - Always first use `read_msg( msgid=msgid, n=0, relative=True, nums=True)` to view the content with line numbers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c927236", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def cycle(self:L):\n", + " \"Same as `itertools.cycle`\"\n", + " return cycle(self)" + ] + }, + { + "cell_type": "markdown", + "id": "da92823d", + "metadata": {}, + "source": [ + "`L.cycle` returns an infinite iterator that cycles through the elements:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "816b9e9c", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(list(itertools.islice(L(1,2,3).cycle(), 7)), [1,2,3,1,2,3,1])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79e84e6d", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def takewhile(self:L, f):\n", + " \"Same as `itertools.takewhile`\"\n", + " return self._new(itertools.takewhile(f, self))" + ] + }, + { + "cell_type": "markdown", + "id": "495570d8", + "metadata": {}, + "source": [ + "`L.takewhile` returns elements from the beginning of the list while the predicate is true:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b80039c", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L(1,2,3,4,5,1,2).takewhile(lambda x: x<4), [1,2,3])\n", + "test_eq(L(1,2,3,11).takewhile(lt(10)), [1,2,3])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9f6e354", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def dropwhile(self:L, f):\n", + " \"Same as `itertools.dropwhile`\"\n", + " return self._new(itertools.dropwhile(f, self))" + ] + }, + { + "cell_type": "markdown", + "id": "60429643", + "metadata": {}, + "source": [ + "`L.dropwhile` skips elements from the beginning while the predicate is true, then returns the rest:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "086eec4a", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L(1,2,3,4,5,1,2).dropwhile(lt(4)), [4,5,1,2])\n", + "test_eq(L(1,2,3).dropwhile(lt(10)), [])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45f04c5c", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def startakewhile(self:L, f):\n", + " \"Like `takewhile`, but unpacks elements as args to `f`\"\n", + " return self._new(itertools.takewhile(lambda x: f(*x), self))" + ] + }, + { + "cell_type": "markdown", + "id": "1a0cb216", + "metadata": {}, + "source": [ + "`L.startakewhile` is like `takewhile`, but unpacks tuple elements as arguments to the predicate:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14007659", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L((1,2),(2,3),(4,1),(5,6)).startakewhile(lambda a,b: a= b" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63c70884", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def rstarpartition(self:L, f, **kwargs):\n", + " \"Like `starpartition`, but reverse the order of args\"\n", + " a,b = [],[]\n", + " for o in self: (a if f(*o[::-1], **kwargs) else b).append(o)\n", + " return self._new(a),self._new(b)" + ] + }, + { + "cell_type": "markdown", + "id": "bcc7d201", + "metadata": {}, + "source": [ + "`L.rstarpartition` is like `starpartition`, but reverses the order of unpacked arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58127227", + "metadata": {}, + "outputs": [], + "source": [ + "asc,desc = L((2,1),(1,3),(4,2),(3,5)).rstarpartition(lt)\n", + "test_eq(asc, [(2,1),(4,2)]) # b < a (i.e., 1<2, 2<4)\n", + "test_eq(desc, [(1,3),(3,5)]) # b >= a" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c269425", + "metadata": {}, + "outputs": [], + "source": [ + "@patch\n", + "def flatten(self:L):\n", + " \"Recursively flatten nested iterables (except strings)\"\n", + " def _flatten(o):\n", + " for item in o:\n", + " if isinstance(item, (str,bytes)) or not hasattr(item,'__iter__'): yield item\n", + " else: yield from _flatten(item)\n", + " return self._new(_flatten(self))" + ] + }, + { + "cell_type": "markdown", + "id": "c943050a", + "metadata": {}, + "source": [ + "`L.flatten` recursively flattens nested iterables into a single `L`. Strings are treated as atomic (not iterated over):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5f233d2", + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(L([[1,2],[3,[4,5]]]).flatten(), [1,2,3,4,5])\n", + "test_eq(L([1,[2,[3,[4]]]]).flatten(), [1,2,3,4])\n", + "test_eq(L(['a',['b','c'],'d']).flatten(), ['a','b','c','d']) # strings not flattened\n", + "test_eq(L([1,2,3]).flatten(), [1,2,3]) # already flat" + ] + }, + { + "cell_type": "markdown", + "id": "ec8624ad", + "metadata": { + "solveit_ai": true, + "use_thinking": true + }, + "source": [ + "Use &`find_msgs` to view all messages, and then tell me which (if any) methods you can see defined that probably should be exported, but haven't been.\n", + "\n", + "##### 🤖Reply🤖\n", + "\n", + "🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠\n", + "\n" + ] + }, { "cell_type": "markdown", "id": "8cf19e29", @@ -2471,7 +3547,10 @@ "source": [] } ], - "metadata": {}, + "metadata": { + "solveit_dialog_mode": "learning", + "solveit_ver": 2 + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 8b9b6592..0176ec44 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -73,7 +73,9 @@ { "cell_type": "markdown", "id": "c8002f11", - "metadata": {}, + "metadata": { + "heading_collapsed": true + }, "source": [ "## File Functions" ] @@ -81,7 +83,9 @@ { "cell_type": "markdown", "id": "565b8a1d", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "Utilities (other than extensions to Pathlib.Path) for dealing with IO." ] @@ -90,7 +94,9 @@ "cell_type": "code", "execution_count": null, "id": "f64c2b09", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -119,7 +125,9 @@ "cell_type": "code", "execution_count": null, "id": "b86f6f56", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -159,8 +167,21 @@ "cell_type": "code", "execution_count": null, "id": "91c9e1df", - "metadata": {}, - "outputs": [], + "metadata": { + "hidden": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#5) ['./fastcore/basics.py','./fastcore/dispatch.py','./fastcore/docments.py','./fastcore/docscrape.py','./fastcore/script.py']" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "globtastic('.', skip_folder_re='^[_.]', folder_re='core', file_glob='*.*py*', file_re='c')" ] @@ -169,7 +190,9 @@ "cell_type": "code", "execution_count": null, "id": "93a1c35d", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -190,7 +213,9 @@ "cell_type": "code", "execution_count": null, "id": "d514da68", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -205,7 +230,9 @@ { "cell_type": "markdown", "id": "1b1e0c94", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "This is useful for functions where you want to accept a path *or* file. `maybe_open` will not close your file handle if you pass one in." ] @@ -214,7 +241,9 @@ "cell_type": "code", "execution_count": null, "id": "27e4f70d", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "def _f(fn):\n", @@ -229,7 +258,9 @@ { "cell_type": "markdown", "id": "6e150f95", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "For example, we can use this to reimplement [`imghdr.what`](https://docs.python.org/3/library/imghdr.html#imghdr.what) from the Python standard library, which is [written in Python 3.9](https://github.com/python/cpython/blob/3.9/Lib/imghdr.py#L11) as:" ] @@ -238,7 +269,9 @@ "cell_type": "code", "execution_count": null, "id": "dbaa6878", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "from fastcore import imghdr" @@ -248,7 +281,9 @@ "cell_type": "code", "execution_count": null, "id": "50e0c494", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "def what(file, h=None):\n", @@ -273,7 +308,9 @@ { "cell_type": "markdown", "id": "61bbdf9d", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "Here's an example of the use of this function:" ] @@ -282,7 +319,9 @@ "cell_type": "code", "execution_count": null, "id": "1a301e49", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -303,7 +342,9 @@ { "cell_type": "markdown", "id": "63fd51fe", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "With `maybe_open`, `Self`, and `L.map_first`, we can rewrite this in a much more concise and (in our opinion) clear way:" ] @@ -312,7 +353,9 @@ "cell_type": "code", "execution_count": null, "id": "2c1557f2", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "def what(file, h=None):\n", @@ -324,7 +367,9 @@ { "cell_type": "markdown", "id": "a136809f", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "...and we can check that it still works:" ] @@ -333,7 +378,9 @@ "cell_type": "code", "execution_count": null, "id": "a443b319", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(what(fname), 'jpeg')" @@ -342,7 +389,9 @@ { "cell_type": "markdown", "id": "00199093", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "...along with the version passing a file handle:" ] @@ -351,7 +400,9 @@ "cell_type": "code", "execution_count": null, "id": "05871c81", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "with open(fname,'rb') as f: test_eq(what(f), 'jpeg')" @@ -360,7 +411,9 @@ { "cell_type": "markdown", "id": "0eed4b0f", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "...along with the `h` parameter version:" ] @@ -369,7 +422,9 @@ "cell_type": "code", "execution_count": null, "id": "8377f8af", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "with open(fname,'rb') as f: test_eq(what(None, h=f.read(32)), 'jpeg')" @@ -379,7 +434,9 @@ "cell_type": "code", "execution_count": null, "id": "63d86576", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -396,7 +453,9 @@ "cell_type": "code", "execution_count": null, "id": "7ca6a87d", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "with tempfile.TemporaryDirectory() as d:\n", @@ -416,7 +475,9 @@ "cell_type": "code", "execution_count": null, "id": "b33313ff", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -449,7 +510,9 @@ "cell_type": "code", "execution_count": null, "id": "2e9a1760", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(image_size(fname), (1200,803))" @@ -459,7 +522,9 @@ "cell_type": "code", "execution_count": null, "id": "c7dc50c4", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "from PIL import Image\n", @@ -470,7 +535,9 @@ "cell_type": "code", "execution_count": null, "id": "1f3c35e2", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -494,7 +561,9 @@ "cell_type": "code", "execution_count": null, "id": "d3aa6edf", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -510,7 +579,9 @@ "cell_type": "code", "execution_count": null, "id": "3d8d71ae", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -533,7 +604,9 @@ "cell_type": "code", "execution_count": null, "id": "e5801198", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -565,7 +638,9 @@ "cell_type": "code", "execution_count": null, "id": "95c46f5a", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -586,7 +661,9 @@ "cell_type": "code", "execution_count": null, "id": "f856746a", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -605,7 +682,9 @@ "cell_type": "code", "execution_count": null, "id": "edd12907", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "f = Path('files/test.txt')\n", @@ -621,7 +700,9 @@ "cell_type": "code", "execution_count": null, "id": "0a78c637", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -637,7 +718,9 @@ "cell_type": "code", "execution_count": null, "id": "131170f7", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -657,7 +740,9 @@ "cell_type": "code", "execution_count": null, "id": "2baed86c", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "tst = \"\"\"\n", @@ -676,7 +761,9 @@ "cell_type": "code", "execution_count": null, "id": "ed688ac4", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -692,7 +779,9 @@ "cell_type": "code", "execution_count": null, "id": "ac84300c", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -707,7 +796,9 @@ "cell_type": "code", "execution_count": null, "id": "6f1e90e9", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -734,7 +825,9 @@ "cell_type": "code", "execution_count": null, "id": "39d4866a", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "def test_untar(foldername, rename=False, **kwargs):\n", @@ -750,7 +843,9 @@ { "cell_type": "markdown", "id": "f57010a9", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "If the contents of `fname` contain just one file or directory, it is placed directly in `dest`:" ] @@ -759,7 +854,9 @@ "cell_type": "code", "execution_count": null, "id": "88501e83", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "# using `base_dir` in `make_archive` results in `images` directory included in file names\n", @@ -769,7 +866,9 @@ { "cell_type": "markdown", "id": "ba67bbc3", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "If `rename` then the directory created is named based on the archive, without extension:" ] @@ -778,7 +877,9 @@ "cell_type": "code", "execution_count": null, "id": "72e2d7a2", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_untar('a', base_dir='images', rename=True)" @@ -787,7 +888,9 @@ { "cell_type": "markdown", "id": "e8937d1f", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "If the contents of `fname` contain multiple files and directories, a new folder in `dest` is created with the same name as `fname` (but without extension):" ] @@ -796,7 +899,9 @@ "cell_type": "code", "execution_count": null, "id": "16235244", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "# using `root_dir` in `make_archive` results in `images` directory *not* included in file names\n", @@ -807,7 +912,9 @@ "cell_type": "code", "execution_count": null, "id": "ab91ee87", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -822,7 +929,9 @@ "cell_type": "code", "execution_count": null, "id": "20f50fcb", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai'])\n", @@ -833,7 +942,9 @@ "cell_type": "code", "execution_count": null, "id": "ba70e535", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -847,7 +958,9 @@ "cell_type": "code", "execution_count": null, "id": "f62134a5", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -861,7 +974,9 @@ "cell_type": "code", "execution_count": null, "id": "cb24780e", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -876,7 +991,9 @@ "cell_type": "code", "execution_count": null, "id": "dda52646", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -907,7 +1024,9 @@ { "cell_type": "markdown", "id": "6704bb62", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "You can pass a string (which will be split based on standard shell rules), a list, or pass args directly:" ] @@ -916,7 +1035,9 @@ "cell_type": "code", "execution_count": null, "id": "8bcce474", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "data": { @@ -939,7 +1060,9 @@ "cell_type": "code", "execution_count": null, "id": "51a8302a", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "if sys.platform == 'win32':\n", @@ -955,7 +1078,9 @@ { "cell_type": "markdown", "id": "f9cc74bc", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "Some commands fail in non-error situations, like `grep`. Use `ignore_ex` in those cases, which will return a tuple of stdout and returncode:" ] @@ -964,7 +1089,9 @@ "cell_type": "code", "execution_count": null, "id": "6567205d", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "if sys.platform == 'win32':\n", @@ -976,7 +1103,9 @@ { "cell_type": "markdown", "id": "d235de1c", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "`run` automatically decodes returned bytes to a `str`. Use `as_bytes` to skip that:" ] @@ -985,7 +1114,9 @@ "cell_type": "code", "execution_count": null, "id": "2a41d502", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "if sys.platform == 'win32':\n", @@ -998,7 +1129,9 @@ "cell_type": "code", "execution_count": null, "id": "5097b830", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1017,7 +1150,9 @@ "cell_type": "code", "execution_count": null, "id": "a9fc7a40", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1031,7 +1166,9 @@ "cell_type": "code", "execution_count": null, "id": "5e9bc30e", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1045,7 +1182,9 @@ "cell_type": "code", "execution_count": null, "id": "59d72612", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "for suf in '.pkl','.bz2','.gz':\n", @@ -1063,7 +1202,9 @@ "cell_type": "code", "execution_count": null, "id": "02029d02", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1082,7 +1223,9 @@ "cell_type": "code", "execution_count": null, "id": "4e176024", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "testf = \"\"\"# comment\n", @@ -1103,7 +1246,9 @@ "cell_type": "code", "execution_count": null, "id": "d030f939", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1145,7 +1290,9 @@ "cell_type": "code", "execution_count": null, "id": "3c593d73", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "inp = \"\"\"from math import *\n", @@ -1182,7 +1329,9 @@ { "cell_type": "markdown", "id": "21654e3e", - "metadata": {}, + "metadata": { + "heading_collapsed": true + }, "source": [ "## Collections" ] @@ -1191,7 +1340,9 @@ "cell_type": "code", "execution_count": null, "id": "7d7f3001", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1205,7 +1356,9 @@ { "cell_type": "markdown", "id": "810bd34d", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "This is a convenience to give you \"dotted\" access to (possibly nested) dictionaries, e.g:" ] @@ -1214,7 +1367,9 @@ "cell_type": "code", "execution_count": null, "id": "f510433e", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "d1 = dict(a=1, b=dict(c=2,d=3))\n", @@ -1226,7 +1381,9 @@ { "cell_type": "markdown", "id": "ff43418b", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "It can also be used on lists of dicts." ] @@ -1235,7 +1392,9 @@ "cell_type": "code", "execution_count": null, "id": "90445bc3", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "_list_of_dicts = [d1, d1]\n", @@ -1247,7 +1406,9 @@ "cell_type": "code", "execution_count": null, "id": "055a1e07", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1261,7 +1422,9 @@ { "cell_type": "markdown", "id": "48b4585c", - "metadata": {}, + "metadata": { + "hidden": true + }, "source": [ "`obj2dict` can be used to reverse what is done by `dict2obj`:" ] @@ -1270,7 +1433,9 @@ "cell_type": "code", "execution_count": null, "id": "5118055b", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "test_eq(obj2dict(d2), d1)\n", @@ -1281,7 +1446,9 @@ "cell_type": "code", "execution_count": null, "id": "36b00c72", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1297,7 +1464,9 @@ "cell_type": "code", "execution_count": null, "id": "13bb8fba", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1310,7 +1479,9 @@ "cell_type": "code", "execution_count": null, "id": "6d4f972b", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [ { "name": "stdout", @@ -1331,7 +1502,9 @@ "cell_type": "code", "execution_count": null, "id": "20842e1f", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1344,7 +1517,9 @@ "cell_type": "code", "execution_count": null, "id": "fb1d1c14", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "assert is_listy((1,))\n", @@ -1358,7 +1533,9 @@ "cell_type": "code", "execution_count": null, "id": "40c2c641", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "#| export\n", @@ -1371,7 +1548,9 @@ "cell_type": "code", "execution_count": null, "id": "ca6571e1", - "metadata": {}, + "metadata": { + "hidden": true + }, "outputs": [], "source": [ "def _f(x,a=1): return x-a\n", @@ -4488,7 +4667,10 @@ "source": [] } ], - "metadata": {}, + "metadata": { + "solveit_dialog_mode": "learning", + "solveit_ver": 2 + }, "nbformat": 4, "nbformat_minor": 5 } From 31819c9b9e7ddbe50c1663fe49d2ed86ff440793 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 11:37:50 +1000 Subject: [PATCH 159/182] export --- fastcore/_modidx.py | 27 ++++ fastcore/foundation.py | 187 ++++++++++++++++++++++ nbs/02_foundation.ipynb | 267 +++++++++---------------------- nbs/03_xtras.ipynb | 341 ++++++++++------------------------------ 4 files changed, 369 insertions(+), 453 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 62300c00..d58118ce 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -342,14 +342,20 @@ 'fastcore.foundation.L._new': ('foundation.html#l._new', 'fastcore/foundation.py'), 'fastcore.foundation.L._repr_pretty_': ('foundation.html#l._repr_pretty_', 'fastcore/foundation.py'), 'fastcore.foundation.L._xtra': ('foundation.html#l._xtra', 'fastcore/foundation.py'), + 'fastcore.foundation.L.accumulate': ('foundation.html#l.accumulate', 'fastcore/foundation.py'), 'fastcore.foundation.L.argfirst': ('foundation.html#l.argfirst', 'fastcore/foundation.py'), 'fastcore.foundation.L.argwhere': ('foundation.html#l.argwhere', 'fastcore/foundation.py'), 'fastcore.foundation.L.attrgot': ('foundation.html#l.attrgot', 'fastcore/foundation.py'), + 'fastcore.foundation.L.batched': ('foundation.html#l.batched', 'fastcore/foundation.py'), + 'fastcore.foundation.L.combinations': ('foundation.html#l.combinations', 'fastcore/foundation.py'), + 'fastcore.foundation.L.compress': ('foundation.html#l.compress', 'fastcore/foundation.py'), 'fastcore.foundation.L.concat': ('foundation.html#l.concat', 'fastcore/foundation.py'), 'fastcore.foundation.L.copy': ('foundation.html#l.copy', 'fastcore/foundation.py'), 'fastcore.foundation.L.cycle': ('foundation.html#l.cycle', 'fastcore/foundation.py'), + 'fastcore.foundation.L.dropwhile': ('foundation.html#l.dropwhile', 'fastcore/foundation.py'), 'fastcore.foundation.L.enumerate': ('foundation.html#l.enumerate', 'fastcore/foundation.py'), 'fastcore.foundation.L.filter': ('foundation.html#l.filter', 'fastcore/foundation.py'), + 'fastcore.foundation.L.flatten': ('foundation.html#l.flatten', 'fastcore/foundation.py'), 'fastcore.foundation.L.groupby': ('foundation.html#l.groupby', 'fastcore/foundation.py'), 'fastcore.foundation.L.itemgot': ('foundation.html#l.itemgot', 'fastcore/foundation.py'), 'fastcore.foundation.L.map': ('foundation.html#l.map', 'fastcore/foundation.py'), @@ -357,17 +363,38 @@ 'fastcore.foundation.L.map_first': ('foundation.html#l.map_first', 'fastcore/foundation.py'), 'fastcore.foundation.L.map_zip': ('foundation.html#l.map_zip', 'fastcore/foundation.py'), 'fastcore.foundation.L.map_zipwith': ('foundation.html#l.map_zipwith', 'fastcore/foundation.py'), + 'fastcore.foundation.L.pairwise': ('foundation.html#l.pairwise', 'fastcore/foundation.py'), + 'fastcore.foundation.L.partition': ('foundation.html#l.partition', 'fastcore/foundation.py'), + 'fastcore.foundation.L.permutations': ('foundation.html#l.permutations', 'fastcore/foundation.py'), 'fastcore.foundation.L.product': ('foundation.html#l.product', 'fastcore/foundation.py'), 'fastcore.foundation.L.range': ('foundation.html#l.range', 'fastcore/foundation.py'), 'fastcore.foundation.L.reduce': ('foundation.html#l.reduce', 'fastcore/foundation.py'), 'fastcore.foundation.L.renumerate': ('foundation.html#l.renumerate', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstarargfirst': ('foundation.html#l.rstarargfirst', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstarargwhere': ('foundation.html#l.rstarargwhere', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstardropwhile': ('foundation.html#l.rstardropwhile', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstarfilter': ('foundation.html#l.rstarfilter', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstarmap': ('foundation.html#l.rstarmap', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstarpartition': ('foundation.html#l.rstarpartition', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstarreduce': ('foundation.html#l.rstarreduce', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstarsorted': ('foundation.html#l.rstarsorted', 'fastcore/foundation.py'), + 'fastcore.foundation.L.rstartakewhile': ('foundation.html#l.rstartakewhile', 'fastcore/foundation.py'), 'fastcore.foundation.L.setattrs': ('foundation.html#l.setattrs', 'fastcore/foundation.py'), 'fastcore.foundation.L.shuffle': ('foundation.html#l.shuffle', 'fastcore/foundation.py'), 'fastcore.foundation.L.sorted': ('foundation.html#l.sorted', 'fastcore/foundation.py'), 'fastcore.foundation.L.split': ('foundation.html#l.split', 'fastcore/foundation.py'), 'fastcore.foundation.L.splitlines': ('foundation.html#l.splitlines', 'fastcore/foundation.py'), + 'fastcore.foundation.L.starargfirst': ('foundation.html#l.starargfirst', 'fastcore/foundation.py'), + 'fastcore.foundation.L.starargwhere': ('foundation.html#l.starargwhere', 'fastcore/foundation.py'), + 'fastcore.foundation.L.stardropwhile': ('foundation.html#l.stardropwhile', 'fastcore/foundation.py'), + 'fastcore.foundation.L.starfilter': ('foundation.html#l.starfilter', 'fastcore/foundation.py'), 'fastcore.foundation.L.starmap': ('foundation.html#l.starmap', 'fastcore/foundation.py'), + 'fastcore.foundation.L.starpartition': ('foundation.html#l.starpartition', 'fastcore/foundation.py'), + 'fastcore.foundation.L.starreduce': ('foundation.html#l.starreduce', 'fastcore/foundation.py'), + 'fastcore.foundation.L.starsorted': ('foundation.html#l.starsorted', 'fastcore/foundation.py'), + 'fastcore.foundation.L.startakewhile': ('foundation.html#l.startakewhile', 'fastcore/foundation.py'), 'fastcore.foundation.L.sum': ('foundation.html#l.sum', 'fastcore/foundation.py'), + 'fastcore.foundation.L.takewhile': ('foundation.html#l.takewhile', 'fastcore/foundation.py'), 'fastcore.foundation.L.unique': ('foundation.html#l.unique', 'fastcore/foundation.py'), 'fastcore.foundation.L.val2idx': ('foundation.html#l.val2idx', 'fastcore/foundation.py'), 'fastcore.foundation.L.zip': ('foundation.html#l.zip', 'fastcore/foundation.py'), diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 5acc288b..b6d048dc 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -202,6 +202,22 @@ def filter(self:L, f=noop, negate=False, **kwargs): "Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`" return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs)) +# %% ../nbs/02_foundation.ipynb +@patch +def starfilter(self:L, f, negate=False, **kwargs): + "Like `filter`, but unpacks elements as args to `f`" + _f = lambda x: f(*x, **kwargs) + if negate: _f = not_(_f) + return self._new(filter(_f, self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def rstarfilter(self:L, f, negate=False, **kwargs): + "Like `starfilter`, but reverse the order of args" + _f = lambda x: f(*x[::-1], **kwargs) + if negate: _f = not_(_f) + return self._new(filter(_f, self)) + # %% ../nbs/02_foundation.ipynb @patch(cls_method=True) def range(cls:L, a, b=None, step=None): @@ -214,6 +230,22 @@ def argwhere(self:L, f, negate=False, **kwargs): "Like `filter`, but return indices for matching items" return self._new(argwhere(self, f, negate, **kwargs)) +# %% ../nbs/02_foundation.ipynb +@patch +def starargwhere(self:L, f, negate=False): + "Like `argwhere`, but unpacks elements as args to `f`" + _f = lambda x: f(*x) + if negate: _f = not_(_f) + return self._new(i for i,o in enumerate(self) if _f(o)) + +# %% ../nbs/02_foundation.ipynb +@patch +def rstarargwhere(self:L, f, negate=False): + "Like `starargwhere`, but reverse the order of args" + _f = lambda x: f(*x[::-1]) + if negate: _f = not_(_f) + return self._new(i for i,o in enumerate(self) if _f(o)) + # %% ../nbs/02_foundation.ipynb @patch def enumerate(self:L): @@ -233,6 +265,22 @@ def argfirst(self:L, f, negate=False): if negate: f = not_(f) return first(i for i,o in self.enumerate() if f(o)) +# %% ../nbs/02_foundation.ipynb +@patch +def starargfirst(self:L, f, negate=False): + "Like `argfirst`, but unpacks elements as args to `f`" + _f = lambda x: f(*x) + if negate: _f = not_(_f) + return first(i for i,o in self.enumerate() if _f(o)) + +# %% ../nbs/02_foundation.ipynb +@patch +def rstarargfirst(self:L, f, negate=False): + "Like `starargfirst`, but reverse the order of args" + _f = lambda x: f(*x[::-1]) + if negate: _f = not_(_f) + return first(i for i,o in self.enumerate() if _f(o)) + # %% ../nbs/02_foundation.ipynb @patch def map(self:L, f, *args, **kwargs): @@ -245,6 +293,12 @@ def starmap(self:L, f, *args, **kwargs): "Like `map`, but use `itertools.starmap`" return self._new(itertools.starmap(partial(f,*args,**kwargs), self)) +# %% ../nbs/02_foundation.ipynb +@patch +def rstarmap(self:L, f, *args, **kwargs): + "Like `starmap`, but reverse the order of args" + return self._new(itertools.starmap(lambda *x: f(*x[::-1], *args, **kwargs), self)) + # %% ../nbs/02_foundation.ipynb @patch def map_dict(self:L, f=noop, *args, **kwargs): @@ -295,6 +349,18 @@ def sorted(self:L, key=None, reverse=False, cmp=None, **kwargs): "New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`" return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs)) +# %% ../nbs/02_foundation.ipynb +@patch +def starsorted(self:L, key, reverse=False): + "Like `sorted`, but unpacks elements as args to `key`" + return self._new(sorted(self, key=lambda x: key(*x), reverse=reverse)) + +# %% ../nbs/02_foundation.ipynb +@patch +def rstarsorted(self:L, key, reverse=False): + "Like `starsorted`, but reverse the order of args" + return self._new(sorted(self, key=lambda x: key(*x[::-1]), reverse=reverse)) + # %% ../nbs/02_foundation.ipynb @patch def concat(self:L): @@ -321,6 +387,20 @@ def reduce(self:L, f, initial=None): "Wrapper for `functools.reduce`" return reduce(f, self) if initial is None else reduce(f, self, initial) +# %% ../nbs/02_foundation.ipynb +@patch +def starreduce(self:L, f, initial=None): + "Like `reduce`, but unpacks elements as args to `f`" + _f = lambda acc, x: f(acc, *x) + return reduce(_f, self) if initial is None else reduce(_f, self, initial) + +# %% ../nbs/02_foundation.ipynb +@patch +def rstarreduce(self:L, f, initial=None): + "Like `starreduce`, but reverse the order of unpacked args" + _f = lambda acc, x: f(acc, *x[::-1]) + return reduce(_f, self) if initial is None else reduce(_f, self, initial) + # %% ../nbs/02_foundation.ipynb @patch def sum(self:L): @@ -351,6 +431,113 @@ def cycle(self:L): "Same as `itertools.cycle`" return cycle(self) +# %% ../nbs/02_foundation.ipynb +@patch +def takewhile(self:L, f): + "Same as `itertools.takewhile`" + return self._new(itertools.takewhile(f, self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def dropwhile(self:L, f): + "Same as `itertools.dropwhile`" + return self._new(itertools.dropwhile(f, self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def startakewhile(self:L, f): + "Like `takewhile`, but unpacks elements as args to `f`" + return self._new(itertools.takewhile(lambda x: f(*x), self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def rstartakewhile(self:L, f): + "Like `startakewhile`, but reverse the order of args" + return self._new(itertools.takewhile(lambda x: f(*x[::-1]), self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def stardropwhile(self:L, f): + "Like `dropwhile`, but unpacks elements as args to `f`" + return self._new(itertools.dropwhile(lambda x: f(*x), self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def rstardropwhile(self:L, f): + "Like `stardropwhile`, but reverse the order of args" + return self._new(itertools.dropwhile(lambda x: f(*x[::-1]), self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def accumulate(self:L, f=operator.add, initial=None): + "Same as `itertools.accumulate`" + return self._new(itertools.accumulate(self, f, initial=initial)) + +# %% ../nbs/02_foundation.ipynb +@patch +def pairwise(self:L): + "Same as `itertools.pairwise`" + return self._new(itertools.pairwise(self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def batched(self:L, n): + "Same as `itertools.batched`" + return self._new(itertools.batched(self, n)) + +# %% ../nbs/02_foundation.ipynb +@patch +def compress(self:L, selectors): + "Same as `itertools.compress`" + return self._new(itertools.compress(self, selectors)) + +# %% ../nbs/02_foundation.ipynb +@patch +def permutations(self:L, r=None): + "Same as `itertools.permutations`" + return self._new(itertools.permutations(self, r)) + +# %% ../nbs/02_foundation.ipynb +@patch +def combinations(self:L, r): + "Same as `itertools.combinations`" + return self._new(itertools.combinations(self, r)) + +# %% ../nbs/02_foundation.ipynb +@patch +def partition(self:L, f=noop, **kwargs): + "Split into two `L`s based on predicate `f`: (true_items, false_items)" + a,b = [],[] + for o in self: (a if f(o, **kwargs) else b).append(o) + return self._new(a),self._new(b) + + +# %% ../nbs/02_foundation.ipynb +@patch +def starpartition(self:L, f, **kwargs): + "Like `partition`, but unpacks elements as args to `f`" + a,b = [],[] + for o in self: (a if f(*o, **kwargs) else b).append(o) + return self._new(a),self._new(b) + +# %% ../nbs/02_foundation.ipynb +@patch +def rstarpartition(self:L, f, **kwargs): + "Like `starpartition`, but reverse the order of args" + a,b = [],[] + for o in self: (a if f(*o[::-1], **kwargs) else b).append(o) + return self._new(a),self._new(b) + +# %% ../nbs/02_foundation.ipynb +@patch +def flatten(self:L): + "Recursively flatten nested iterables (except strings)" + def _flatten(o): + for item in o: + if isinstance(item, (str,bytes)) or not hasattr(item,'__iter__'): yield item + else: yield from _flatten(item) + return self._new(_flatten(self)) + # %% ../nbs/02_foundation.ipynb def save_config_file(file, d, **kwargs): "Write settings dict to a new config file, or overwrite the existing one." diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 94c6ebe8..4ed50ecc 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -53,9 +53,7 @@ { "cell_type": "markdown", "id": "13ac17ff", - "metadata": { - "heading_collapsed": true - }, + "metadata": {}, "source": [ "## Foundational Functions" ] @@ -64,9 +62,7 @@ "cell_type": "code", "execution_count": null, "id": "f059df0f", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -83,9 +79,7 @@ "cell_type": "code", "execution_count": null, "id": "c3fd027f", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -106,9 +100,7 @@ { "cell_type": "markdown", "id": "620dc00e", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "`add_docs` allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in [our style guide](https://docs.fast.ai/dev/style.html).\n", "\n", @@ -119,9 +111,7 @@ "cell_type": "code", "execution_count": null, "id": "0832c567", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "class T:\n", @@ -132,9 +122,7 @@ { "cell_type": "markdown", "id": "adedd47e", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "You can add documentation to this class like so:" ] @@ -143,9 +131,7 @@ "cell_type": "code", "execution_count": null, "id": "da9d95f9", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "add_docs(T, cls_doc=\"A docstring for the class.\",\n", @@ -156,9 +142,7 @@ { "cell_type": "markdown", "id": "a91ee046", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "Now, docstrings will appear as expected:" ] @@ -167,9 +151,7 @@ "cell_type": "code", "execution_count": null, "id": "2c4a26aa", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(T.__doc__, \"A docstring for the class.\")\n", @@ -180,9 +162,7 @@ { "cell_type": "markdown", "id": "47545904", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "`add_docs` also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:" ] @@ -191,9 +171,7 @@ "cell_type": "code", "execution_count": null, "id": "a9f77cf6", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "class T:\n", @@ -208,9 +186,7 @@ "cell_type": "code", "execution_count": null, "id": "88805b49", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| hide\n", @@ -229,9 +205,7 @@ "cell_type": "code", "execution_count": null, "id": "b180313a", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -244,9 +218,7 @@ { "cell_type": "markdown", "id": "e25bd40f", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "Instead of using `add_docs`, you can use the decorator `docs` as shown below. Note that the docstring for the class can be set with the argument `cls_doc`:" ] @@ -255,9 +227,7 @@ "cell_type": "code", "execution_count": null, "id": "d10f59f9", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "@docs\n", @@ -278,9 +248,7 @@ { "cell_type": "markdown", "id": "2ad622b1", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "For either the `docs` decorator or the `add_docs` function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the `_docs` attribute:" ] @@ -289,9 +257,7 @@ "cell_type": "code", "execution_count": null, "id": "e4bbd1b9", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "@docs\n", @@ -309,9 +275,7 @@ "cell_type": "code", "execution_count": null, "id": "0b1f9829", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -343,9 +307,7 @@ "cell_type": "code", "execution_count": null, "id": "4e191b5b", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "assert is_iter([1])\n", @@ -358,9 +320,7 @@ "cell_type": "code", "execution_count": null, "id": "34a29b43", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -373,9 +333,7 @@ { "cell_type": "markdown", "id": "198216f8", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "`coll_repr` is used to provide a more informative [`__repr__`](https://stackoverflow.com/questions/1984162/purpose-of-pythons-repr) about list-like objects. `coll_repr` and is used by `L` to build a `__repr__` that displays the length of a list in addition to a preview of a list.\n", "\n", @@ -386,9 +344,7 @@ "cell_type": "code", "execution_count": null, "id": "fe3ed954", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(coll_repr(range(1000),10), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')\n", @@ -401,9 +357,7 @@ "cell_type": "code", "execution_count": null, "id": "6cf96f49", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -416,9 +370,7 @@ "cell_type": "code", "execution_count": null, "id": "66a28a53", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -437,9 +389,7 @@ "cell_type": "code", "execution_count": null, "id": "3b7dd826", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(mask2idxs([False,True,False,True]), [1,3])\n", @@ -451,9 +401,7 @@ "cell_type": "code", "execution_count": null, "id": "af754019", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -467,9 +415,7 @@ "cell_type": "code", "execution_count": null, "id": "7f8805d7", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])\n", @@ -482,9 +428,7 @@ "cell_type": "code", "execution_count": null, "id": "9346b91a", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -497,9 +441,7 @@ "cell_type": "code", "execution_count": null, "id": "21c554a1", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])" @@ -509,9 +451,7 @@ "cell_type": "code", "execution_count": null, "id": "8a281173", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -523,9 +463,7 @@ { "cell_type": "markdown", "id": "2c4497d9", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "You can, for example index a single item in a list with an integer or a 0-dimensional numpy array:" ] @@ -534,9 +472,7 @@ "cell_type": "code", "execution_count": null, "id": "cf94cb54", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "assert is_indexer(1)\n", @@ -546,9 +482,7 @@ { "cell_type": "markdown", "id": "504d89e8", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "However, you cannot index into single item in a list with another list or a numpy array with ndim > 0. " ] @@ -557,9 +491,7 @@ "cell_type": "code", "execution_count": null, "id": "1159335e", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "assert not is_indexer([1, 2])\n", @@ -570,9 +502,7 @@ "cell_type": "code", "execution_count": null, "id": "3fd3133f", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -585,9 +515,7 @@ "cell_type": "code", "execution_count": null, "id": "73a52d33", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -608,9 +536,7 @@ "cell_type": "code", "execution_count": null, "id": "fa5990de", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -631,9 +557,7 @@ "cell_type": "code", "execution_count": null, "id": "6f9ef4e6", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -653,9 +577,7 @@ { "cell_type": "markdown", "id": "d8a026cf", - "metadata": { - "heading_collapsed": true - }, + "metadata": {}, "source": [ "## `L` helpers" ] @@ -664,9 +586,7 @@ "cell_type": "code", "execution_count": null, "id": "eefd2763", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -684,9 +604,7 @@ { "cell_type": "markdown", "id": "0ec47e38", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "`ColBase` is a base class that emulates the functionality of a python `list`:" ] @@ -695,9 +613,7 @@ "cell_type": "code", "execution_count": null, "id": "896956c4", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "class _T(CollBase): pass\n", @@ -1406,6 +1322,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def starfilter(self:L, f, negate=False, **kwargs):\n", " \"Like `filter`, but unpacks elements as args to `f`\"\n", @@ -1440,6 +1357,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstarfilter(self:L, f, negate=False, **kwargs):\n", " \"Like `starfilter`, but reverse the order of args\"\n", @@ -1523,6 +1441,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def starargwhere(self:L, f, negate=False):\n", " \"Like `argwhere`, but unpacks elements as args to `f`\"\n", @@ -1557,6 +1476,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstarargwhere(self:L, f, negate=False):\n", " \"Like `starargwhere`, but reverse the order of args\"\n", @@ -1665,6 +1585,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def starargfirst(self:L, f, negate=False):\n", " \"Like `argfirst`, but unpacks elements as args to `f`\"\n", @@ -1699,6 +1620,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstarargfirst(self:L, f, negate=False):\n", " \"Like `starargfirst`, but reverse the order of args\"\n", @@ -1845,6 +1767,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstarmap(self:L, f, *args, **kwargs):\n", " \"Like `starmap`, but reverse the order of args\"\n", @@ -2094,6 +2017,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def starsorted(self:L, key, reverse=False):\n", " \"Like `sorted`, but unpacks elements as args to `key`\"\n", @@ -2126,6 +2050,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstarsorted(self:L, key, reverse=False):\n", " \"Like `starsorted`, but reverse the order of args\"\n", @@ -2269,6 +2194,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def starreduce(self:L, f, initial=None):\n", " \"Like `reduce`, but unpacks elements as args to `f`\"\n", @@ -2332,6 +2258,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstarreduce(self:L, f, initial=None):\n", " \"Like `starreduce`, but reverse the order of unpacked args\"\n", @@ -2456,57 +2383,6 @@ "### itertools wrappers" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "b7179fe6", - "metadata": { - "time_run": "2025-12-13T01:35:17.762331+00:00" - }, - "outputs": [], - "source": [ - "from dialoghelper import *\n", - "dh_settings['port']=6001" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "daf6d8e4", - "metadata": {}, - "outputs": [], - "source": [ - "# tool_info()" - ] - }, - { - "cell_type": "markdown", - "id": "c96ed60e", - "metadata": {}, - "source": [ - "Tools available from `dialoghelper`:\n", - "\n", - "- &`curr_dialog`: Get the current dialog info.\n", - "- &`msg_idx`: Get absolute index of message in dialog.\n", - "- &`add_html`: Send HTML to the browser to be swapped into the DOM using hx-swap-oob.\n", - "- &`find_msg_id`: Get the current message id.\n", - "- &`find_msgs`: Find messages in current specific dialog that contain the given information.\n", - " - (solveit can often get this id directly from its context, and will not need to use this if the required information is already available to it.)\n", - "- &`read_msg`: Get the message indexed in the current dialog.\n", - " - To get the exact message use `n=0` and `relative=True` together with `msgid`.\n", - " - To get a relative message use `n` (relative position index).\n", - " - To get the nth message use `n` with `relative=False`, e.g `n=0` first message, `n=-1` last message.\n", - "- &`del_msg`: Delete a message from the dialog.\n", - "- &`add_msg`: Add/update a message to the queue to show after code execution completes.\n", - "- &`update_msg`: Update an existing message.\n", - "- &`url2note`: Read URL as markdown, and add a note below current message with the result\n", - "- &`msg_insert_line`: Insert text at a specific location in a message.\n", - "- &`msg_str_replace`: Find and replace text in a message.\n", - "- &`msg_strs_replace`: Find and replace multiple strings in a message.\n", - "- &`msg_replace_lines`: Replace a range of lines in a message with new content.\n", - " - Always first use `read_msg( msgid=msgid, n=0, relative=True, nums=True)` to view the content with line numbers." - ] - }, { "cell_type": "code", "execution_count": null, @@ -2546,6 +2422,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def takewhile(self:L, f):\n", " \"Same as `itertools.takewhile`\"\n", @@ -2578,6 +2455,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def dropwhile(self:L, f):\n", " \"Same as `itertools.dropwhile`\"\n", @@ -2610,6 +2488,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def startakewhile(self:L, f):\n", " \"Like `takewhile`, but unpacks elements as args to `f`\"\n", @@ -2642,6 +2521,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstartakewhile(self:L, f):\n", " \"Like `startakewhile`, but reverse the order of args\"\n", @@ -2674,6 +2554,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def stardropwhile(self:L, f):\n", " \"Like `dropwhile`, but unpacks elements as args to `f`\"\n", @@ -2706,6 +2587,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstardropwhile(self:L, f):\n", " \"Like `stardropwhile`, but reverse the order of args\"\n", @@ -2738,6 +2620,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def accumulate(self:L, f=operator.add, initial=None):\n", " \"Same as `itertools.accumulate`\"\n", @@ -2771,6 +2654,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def pairwise(self:L):\n", " \"Same as `itertools.pairwise`\"\n", @@ -2803,6 +2687,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def batched(self:L, n):\n", " \"Same as `itertools.batched`\"\n", @@ -2835,6 +2720,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def compress(self:L, selectors):\n", " \"Same as `itertools.compress`\"\n", @@ -2867,6 +2753,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def permutations(self:L, r=None):\n", " \"Same as `itertools.permutations`\"\n", @@ -2899,6 +2786,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def combinations(self:L, r):\n", " \"Same as `itertools.combinations`\"\n", @@ -2931,6 +2819,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def partition(self:L, f=noop, **kwargs):\n", " \"Split into two `L`s based on predicate `f`: (true_items, false_items)\"\n", @@ -2970,6 +2859,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def starpartition(self:L, f, **kwargs):\n", " \"Like `partition`, but unpacks elements as args to `f`\"\n", @@ -3005,6 +2895,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def rstarpartition(self:L, f, **kwargs):\n", " \"Like `starpartition`, but reverse the order of args\"\n", @@ -3040,6 +2931,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| export\n", "@patch\n", "def flatten(self:L):\n", " \"Recursively flatten nested iterables (except strings)\"\n", @@ -3071,22 +2963,6 @@ "test_eq(L([1,2,3]).flatten(), [1,2,3]) # already flat" ] }, - { - "cell_type": "markdown", - "id": "ec8624ad", - "metadata": { - "solveit_ai": true, - "use_thinking": true - }, - "source": [ - "Use &`find_msgs` to view all messages, and then tell me which (if any) methods you can see defined that probably should be exported, but haven't been.\n", - "\n", - "##### 🤖Reply🤖\n", - "\n", - "🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠\n", - "\n" - ] - }, { "cell_type": "markdown", "id": "8cf19e29", @@ -3547,10 +3423,7 @@ "source": [] } ], - "metadata": { - "solveit_dialog_mode": "learning", - "solveit_ver": 2 - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 0176ec44..0ebebcf7 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -73,9 +73,7 @@ { "cell_type": "markdown", "id": "c8002f11", - "metadata": { - "heading_collapsed": true - }, + "metadata": {}, "source": [ "## File Functions" ] @@ -83,9 +81,7 @@ { "cell_type": "markdown", "id": "565b8a1d", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "Utilities (other than extensions to Pathlib.Path) for dealing with IO." ] @@ -94,9 +90,7 @@ "cell_type": "code", "execution_count": null, "id": "f64c2b09", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -125,9 +119,7 @@ "cell_type": "code", "execution_count": null, "id": "b86f6f56", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -167,9 +159,7 @@ "cell_type": "code", "execution_count": null, "id": "91c9e1df", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -190,9 +180,7 @@ "cell_type": "code", "execution_count": null, "id": "93a1c35d", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -213,9 +201,7 @@ "cell_type": "code", "execution_count": null, "id": "d514da68", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -230,9 +216,7 @@ { "cell_type": "markdown", "id": "1b1e0c94", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "This is useful for functions where you want to accept a path *or* file. `maybe_open` will not close your file handle if you pass one in." ] @@ -241,9 +225,7 @@ "cell_type": "code", "execution_count": null, "id": "27e4f70d", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "def _f(fn):\n", @@ -258,9 +240,7 @@ { "cell_type": "markdown", "id": "6e150f95", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "For example, we can use this to reimplement [`imghdr.what`](https://docs.python.org/3/library/imghdr.html#imghdr.what) from the Python standard library, which is [written in Python 3.9](https://github.com/python/cpython/blob/3.9/Lib/imghdr.py#L11) as:" ] @@ -269,9 +249,7 @@ "cell_type": "code", "execution_count": null, "id": "dbaa6878", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "from fastcore import imghdr" @@ -281,9 +259,7 @@ "cell_type": "code", "execution_count": null, "id": "50e0c494", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "def what(file, h=None):\n", @@ -308,9 +284,7 @@ { "cell_type": "markdown", "id": "61bbdf9d", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "Here's an example of the use of this function:" ] @@ -319,9 +293,7 @@ "cell_type": "code", "execution_count": null, "id": "1a301e49", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -342,9 +314,7 @@ { "cell_type": "markdown", "id": "63fd51fe", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "With `maybe_open`, `Self`, and `L.map_first`, we can rewrite this in a much more concise and (in our opinion) clear way:" ] @@ -353,9 +323,7 @@ "cell_type": "code", "execution_count": null, "id": "2c1557f2", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "def what(file, h=None):\n", @@ -367,9 +335,7 @@ { "cell_type": "markdown", "id": "a136809f", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "...and we can check that it still works:" ] @@ -378,9 +344,7 @@ "cell_type": "code", "execution_count": null, "id": "a443b319", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(what(fname), 'jpeg')" @@ -389,9 +353,7 @@ { "cell_type": "markdown", "id": "00199093", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "...along with the version passing a file handle:" ] @@ -400,9 +362,7 @@ "cell_type": "code", "execution_count": null, "id": "05871c81", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "with open(fname,'rb') as f: test_eq(what(f), 'jpeg')" @@ -411,9 +371,7 @@ { "cell_type": "markdown", "id": "0eed4b0f", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "...along with the `h` parameter version:" ] @@ -422,9 +380,7 @@ "cell_type": "code", "execution_count": null, "id": "8377f8af", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "with open(fname,'rb') as f: test_eq(what(None, h=f.read(32)), 'jpeg')" @@ -434,9 +390,7 @@ "cell_type": "code", "execution_count": null, "id": "63d86576", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -453,9 +407,7 @@ "cell_type": "code", "execution_count": null, "id": "7ca6a87d", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "with tempfile.TemporaryDirectory() as d:\n", @@ -475,9 +427,7 @@ "cell_type": "code", "execution_count": null, "id": "b33313ff", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -510,9 +460,7 @@ "cell_type": "code", "execution_count": null, "id": "2e9a1760", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(image_size(fname), (1200,803))" @@ -522,9 +470,7 @@ "cell_type": "code", "execution_count": null, "id": "c7dc50c4", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "from PIL import Image\n", @@ -535,9 +481,7 @@ "cell_type": "code", "execution_count": null, "id": "1f3c35e2", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -561,9 +505,7 @@ "cell_type": "code", "execution_count": null, "id": "d3aa6edf", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -579,9 +521,7 @@ "cell_type": "code", "execution_count": null, "id": "3d8d71ae", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -604,9 +544,7 @@ "cell_type": "code", "execution_count": null, "id": "e5801198", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -638,9 +576,7 @@ "cell_type": "code", "execution_count": null, "id": "95c46f5a", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -661,9 +597,7 @@ "cell_type": "code", "execution_count": null, "id": "f856746a", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -682,9 +616,7 @@ "cell_type": "code", "execution_count": null, "id": "edd12907", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "f = Path('files/test.txt')\n", @@ -700,9 +632,7 @@ "cell_type": "code", "execution_count": null, "id": "0a78c637", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -718,9 +648,7 @@ "cell_type": "code", "execution_count": null, "id": "131170f7", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -740,9 +668,7 @@ "cell_type": "code", "execution_count": null, "id": "2baed86c", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "tst = \"\"\"\n", @@ -761,9 +687,7 @@ "cell_type": "code", "execution_count": null, "id": "ed688ac4", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -779,9 +703,7 @@ "cell_type": "code", "execution_count": null, "id": "ac84300c", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -796,9 +718,7 @@ "cell_type": "code", "execution_count": null, "id": "6f1e90e9", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -825,9 +745,7 @@ "cell_type": "code", "execution_count": null, "id": "39d4866a", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "def test_untar(foldername, rename=False, **kwargs):\n", @@ -843,9 +761,7 @@ { "cell_type": "markdown", "id": "f57010a9", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "If the contents of `fname` contain just one file or directory, it is placed directly in `dest`:" ] @@ -854,9 +770,7 @@ "cell_type": "code", "execution_count": null, "id": "88501e83", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "# using `base_dir` in `make_archive` results in `images` directory included in file names\n", @@ -866,9 +780,7 @@ { "cell_type": "markdown", "id": "ba67bbc3", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "If `rename` then the directory created is named based on the archive, without extension:" ] @@ -877,9 +789,7 @@ "cell_type": "code", "execution_count": null, "id": "72e2d7a2", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_untar('a', base_dir='images', rename=True)" @@ -888,9 +798,7 @@ { "cell_type": "markdown", "id": "e8937d1f", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "If the contents of `fname` contain multiple files and directories, a new folder in `dest` is created with the same name as `fname` (but without extension):" ] @@ -899,9 +807,7 @@ "cell_type": "code", "execution_count": null, "id": "16235244", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "# using `root_dir` in `make_archive` results in `images` directory *not* included in file names\n", @@ -912,9 +818,7 @@ "cell_type": "code", "execution_count": null, "id": "ab91ee87", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -929,9 +833,7 @@ "cell_type": "code", "execution_count": null, "id": "20f50fcb", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(repo_details('https://github.com/fastai/fastai.git'), ['fastai', 'fastai'])\n", @@ -942,9 +844,7 @@ "cell_type": "code", "execution_count": null, "id": "ba70e535", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -958,9 +858,7 @@ "cell_type": "code", "execution_count": null, "id": "f62134a5", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -974,9 +872,7 @@ "cell_type": "code", "execution_count": null, "id": "cb24780e", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -991,9 +887,7 @@ "cell_type": "code", "execution_count": null, "id": "dda52646", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1024,9 +918,7 @@ { "cell_type": "markdown", "id": "6704bb62", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "You can pass a string (which will be split based on standard shell rules), a list, or pass args directly:" ] @@ -1035,9 +927,7 @@ "cell_type": "code", "execution_count": null, "id": "8bcce474", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "data": { @@ -1060,9 +950,7 @@ "cell_type": "code", "execution_count": null, "id": "51a8302a", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "if sys.platform == 'win32':\n", @@ -1078,9 +966,7 @@ { "cell_type": "markdown", "id": "f9cc74bc", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "Some commands fail in non-error situations, like `grep`. Use `ignore_ex` in those cases, which will return a tuple of stdout and returncode:" ] @@ -1089,9 +975,7 @@ "cell_type": "code", "execution_count": null, "id": "6567205d", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "if sys.platform == 'win32':\n", @@ -1103,9 +987,7 @@ { "cell_type": "markdown", "id": "d235de1c", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "`run` automatically decodes returned bytes to a `str`. Use `as_bytes` to skip that:" ] @@ -1114,9 +996,7 @@ "cell_type": "code", "execution_count": null, "id": "2a41d502", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "if sys.platform == 'win32':\n", @@ -1129,9 +1009,7 @@ "cell_type": "code", "execution_count": null, "id": "5097b830", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1150,9 +1028,7 @@ "cell_type": "code", "execution_count": null, "id": "a9fc7a40", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1166,9 +1042,7 @@ "cell_type": "code", "execution_count": null, "id": "5e9bc30e", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1182,9 +1056,7 @@ "cell_type": "code", "execution_count": null, "id": "59d72612", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "for suf in '.pkl','.bz2','.gz':\n", @@ -1202,9 +1074,7 @@ "cell_type": "code", "execution_count": null, "id": "02029d02", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1223,9 +1093,7 @@ "cell_type": "code", "execution_count": null, "id": "4e176024", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "testf = \"\"\"# comment\n", @@ -1246,9 +1114,7 @@ "cell_type": "code", "execution_count": null, "id": "d030f939", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1290,9 +1156,7 @@ "cell_type": "code", "execution_count": null, "id": "3c593d73", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "inp = \"\"\"from math import *\n", @@ -1329,9 +1193,7 @@ { "cell_type": "markdown", "id": "21654e3e", - "metadata": { - "heading_collapsed": true - }, + "metadata": {}, "source": [ "## Collections" ] @@ -1340,9 +1202,7 @@ "cell_type": "code", "execution_count": null, "id": "7d7f3001", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1356,9 +1216,7 @@ { "cell_type": "markdown", "id": "810bd34d", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "This is a convenience to give you \"dotted\" access to (possibly nested) dictionaries, e.g:" ] @@ -1367,9 +1225,7 @@ "cell_type": "code", "execution_count": null, "id": "f510433e", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "d1 = dict(a=1, b=dict(c=2,d=3))\n", @@ -1381,9 +1237,7 @@ { "cell_type": "markdown", "id": "ff43418b", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "It can also be used on lists of dicts." ] @@ -1392,9 +1246,7 @@ "cell_type": "code", "execution_count": null, "id": "90445bc3", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "_list_of_dicts = [d1, d1]\n", @@ -1406,9 +1258,7 @@ "cell_type": "code", "execution_count": null, "id": "055a1e07", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1422,9 +1272,7 @@ { "cell_type": "markdown", "id": "48b4585c", - "metadata": { - "hidden": true - }, + "metadata": {}, "source": [ "`obj2dict` can be used to reverse what is done by `dict2obj`:" ] @@ -1433,9 +1281,7 @@ "cell_type": "code", "execution_count": null, "id": "5118055b", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(obj2dict(d2), d1)\n", @@ -1446,9 +1292,7 @@ "cell_type": "code", "execution_count": null, "id": "36b00c72", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1464,9 +1308,7 @@ "cell_type": "code", "execution_count": null, "id": "13bb8fba", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1479,9 +1321,7 @@ "cell_type": "code", "execution_count": null, "id": "6d4f972b", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -1502,9 +1342,7 @@ "cell_type": "code", "execution_count": null, "id": "20842e1f", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1517,9 +1355,7 @@ "cell_type": "code", "execution_count": null, "id": "fb1d1c14", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "assert is_listy((1,))\n", @@ -1533,9 +1369,7 @@ "cell_type": "code", "execution_count": null, "id": "40c2c641", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1548,9 +1382,7 @@ "cell_type": "code", "execution_count": null, "id": "ca6571e1", - "metadata": { - "hidden": true - }, + "metadata": {}, "outputs": [], "source": [ "def _f(x,a=1): return x-a\n", @@ -4667,10 +4499,7 @@ "source": [] } ], - "metadata": { - "solveit_dialog_mode": "learning", - "solveit_ver": 2 - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } From e60d78ffade3ec010d5d95894254fe2d8cc26523 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 11:39:44 +1000 Subject: [PATCH 160/182] release --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 793996db..6dd2a7b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ +## 1.8.18 + +### New Features + +- Add more itertools etc to `L` ([#714](https://github.com/AnswerDotAI/fastcore/issues/714)) + +### Bugs Squashed + +- missing import in `detect_mime` ([#712](https://github.com/AnswerDotAI/fastcore/issues/712)) + + ## 1.8.17 ### New Features From 2638456bb7f3cdce3100d72efa75e70ff85ce0f0 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 11:41:03 +1000 Subject: [PATCH 161/182] release --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dd2a7b6..b104eb0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ + + ## 1.8.18 ### New Features From 963b4c71f847ffd710420d94400cff68037b963b Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 11:41:17 +1000 Subject: [PATCH 162/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 91c1a023..608978d6 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.18" +__version__ = "1.8.19" diff --git a/settings.ini b/settings.ini index 2f52e383..e44c3d7c 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.18 +version = 1.8.19 min_python = 3.10 audience = Developers language = English From 4a82f86c8901877cbb37d06850292abcba38b7a5 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 14:00:00 +1000 Subject: [PATCH 163/182] fixes #715 --- fastcore/__init__.py | 2 +- fastcore/_modidx.py | 4 +- fastcore/basics.py | 54 +++++++++-------- fastcore/foundation.py | 2 +- nbs/01_basics.ipynb | 130 +++++++++++++++++++++++++++++++++------- nbs/02_foundation.ipynb | 39 ++++-------- nbs/03_xtras.ipynb | 67 +++++++++------------ nbs/03b_net.ipynb | 28 +++------ settings.ini | 2 +- 9 files changed, 187 insertions(+), 141 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 608978d6..0a0a43a5 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.8.19" +__version__ = "1.9.0" diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index d58118ce..ba017c5f 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -100,6 +100,7 @@ 'fastcore.basics._oper': ('basics.html#_oper', 'fastcore/basics.py'), 'fastcore.basics._risinstance': ('basics.html#_risinstance', 'fastcore/basics.py'), 'fastcore.basics._store_attr': ('basics.html#_store_attr', 'fastcore/basics.py'), + 'fastcore.basics._strip_patch_name': ('basics.html#_strip_patch_name', 'fastcore/basics.py'), 'fastcore.basics._typeerr': ('basics.html#_typeerr', 'fastcore/basics.py'), 'fastcore.basics._using_attr': ('basics.html#_using_attr', 'fastcore/basics.py'), 'fastcore.basics.add_props': ('basics.html#add_props', 'fastcore/basics.py'), @@ -182,7 +183,6 @@ 'fastcore.basics.partition': ('basics.html#partition', 'fastcore/basics.py'), 'fastcore.basics.partition_dict': ('basics.html#partition_dict', 'fastcore/basics.py'), 'fastcore.basics.patch': ('basics.html#patch', 'fastcore/basics.py'), - 'fastcore.basics.patch_property': ('basics.html#patch_property', 'fastcore/basics.py'), 'fastcore.basics.patch_to': ('basics.html#patch_to', 'fastcore/basics.py'), 'fastcore.basics.properties': ('basics.html#properties', 'fastcore/basics.py'), 'fastcore.basics.range_of': ('basics.html#range_of', 'fastcore/basics.py'), @@ -366,7 +366,7 @@ 'fastcore.foundation.L.pairwise': ('foundation.html#l.pairwise', 'fastcore/foundation.py'), 'fastcore.foundation.L.partition': ('foundation.html#l.partition', 'fastcore/foundation.py'), 'fastcore.foundation.L.permutations': ('foundation.html#l.permutations', 'fastcore/foundation.py'), - 'fastcore.foundation.L.product': ('foundation.html#l.product', 'fastcore/foundation.py'), + 'fastcore.foundation.L.product__': ('foundation.html#l.product__', 'fastcore/foundation.py'), 'fastcore.foundation.L.range': ('foundation.html#l.range', 'fastcore/foundation.py'), 'fastcore.foundation.L.reduce': ('foundation.html#l.reduce', 'fastcore/foundation.py'), 'fastcore.foundation.L.renumerate': ('foundation.html#l.renumerate', 'fastcore/foundation.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index db8106c5..7f1be768 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -17,10 +17,10 @@ 'nested_attr', 'nested_setdefault', 'nested_callable', 'nested_idx', 'set_nested_idx', 'val2idx', 'uniqueify', 'loop_first_last', 'loop_first', 'loop_last', 'first_match', 'last_match', 'fastuple', 'bind', 'mapt', 'map_ex', 'compose', 'maps', 'partialler', 'instantiate', 'using_attr', 'copy_func', 'patch_to', - 'patch', 'patch_property', 'compile_re', 'ImportEnum', 'StrEnum', 'str_enum', 'ValEnum', 'Stateful', - 'NotStr', 'PrettyString', 'even_mults', 'num_cpus', 'add_props', 'str2bool', 'str2int', 'str2float', - 'str2list', 'str2date', 'to_bool', 'to_int', 'to_float', 'to_list', 'to_date', 'typed', 'exec_new', - 'exec_import', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', 'add', 'sub', 'mul', 'truediv', 'is_', 'is_not', 'mod'] + 'patch', 'compile_re', 'ImportEnum', 'StrEnum', 'str_enum', 'ValEnum', 'Stateful', 'NotStr', 'PrettyString', + 'even_mults', 'num_cpus', 'add_props', 'str2bool', 'str2int', 'str2float', 'str2list', 'str2date', 'to_bool', + 'to_int', 'to_float', 'to_list', 'to_date', 'typed', 'exec_new', 'exec_import', 'lt', 'gt', 'le', 'ge', 'eq', + 'ne', 'add', 'sub', 'mul', 'truediv', 'is_', 'is_not', 'mod'] # %% ../nbs/01_basics.ipynb from .imports import * @@ -1051,42 +1051,46 @@ def __init__(self, f): self.f = f def __get__(self, _, f_cls): return MethodType(self.f, f_cls) # %% ../nbs/01_basics.ipynb -def patch_to(cls, as_prop=False, cls_method=False, set_prop=False): +def _strip_patch_name(nm): + "Strip trailing `__` from `nm` if it doesn't start with `_`" + return nm[:-2] if nm.endswith('__') and not nm.startswith('_') else nm + +def patch_to(cls, as_prop=False, cls_method=False, set_prop=False, nm=None): "Decorator: add `f` to `cls`" if not isinstance(cls, (tuple,list)): cls=(cls,) def _inner(f): for c_ in cls: nf = copy_func(f) - nm = f.__name__ + fnm = nm or _strip_patch_name(f.__name__) # `functools.update_wrapper` when passing patched function to `Pipeline`, so we do it manually for o in functools.WRAPPER_ASSIGNMENTS: setattr(nf, o, getattr(f,o)) - nf.__qualname__ = f"{c_.__name__}.{nm}" - if cls_method: setattr(c_, nm, _clsmethod(nf)) + nf.__name__ = fnm + nf.__qualname__ = f"{c_.__name__}.{fnm}" + if cls_method: setattr(c_, fnm, _clsmethod(nf)) else: - if set_prop: setattr(c_, nm, getattr(c_, nm).setter(nf)) - elif as_prop: setattr(c_, nm, property(nf)) + if set_prop: setattr(c_, fnm, getattr(c_, fnm).setter(nf)) + elif as_prop: setattr(c_, fnm, property(nf)) else: - onm = '_orig_'+nm - if hasattr(c_, nm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, nm)) - setattr(c_, nm, nf) + onm = '_orig_'+fnm + if hasattr(c_, fnm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, fnm)) + setattr(c_, fnm, nf) # Avoid clobbering existing functions - return globals().get(nm, builtins.__dict__.get(nm, None)) + return globals().get(fnm, builtins.__dict__.get(fnm, None)) return _inner # %% ../nbs/01_basics.ipynb -def patch(f=None, *, as_prop=False, cls_method=False, set_prop=False): +def patch(f=None, *, as_prop=False, cls_method=False, set_prop=False, nm=None): "Decorator: add `f` to the first parameter's class (based on f's type annotations)" - if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop) + if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop, nm=nm) ann,glb,loc = get_annotations_ex(f) - cls = union2tuple(eval_type(ann.pop('cls') if cls_method else next(iter(ann.values())), glb, loc)) - return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop)(f) - -# %% ../nbs/01_basics.ipynb -def patch_property(f): - "Deprecated; use `patch(as_prop=True)` instead" - warnings.warn("`patch_property` is deprecated and will be removed; use `patch(as_prop=True)` instead") - cls = next(iter(f.__annotations__.values())) - return patch_to(cls, as_prop=True)(f) + if cls_method: + if 'cls' not in ann: raise TypeError(f"@patch with cls_method=True requires 'cls' to have a type annotation") + cls = ann.pop('cls') + else: + if not ann: raise TypeError(f"@patch requires the first parameter of `{f.__name__}` to have a type annotation") + cls = next(iter(ann.values())) + cls = union2tuple(eval_type(cls, glb, loc)) + return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop, nm=nm)(f) # %% ../nbs/01_basics.ipynb def compile_re(pat): diff --git a/fastcore/foundation.py b/fastcore/foundation.py index b6d048dc..35673552 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -409,7 +409,7 @@ def sum(self:L): # %% ../nbs/02_foundation.ipynb @patch -def product(self:L): +def product__(self:L): "Product of the items" return self.reduce(operator.mul, 1) diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index e0096a58..b6a9f37a 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -6035,31 +6035,36 @@ { "cell_type": "code", "execution_count": null, - "id": "2256bd09", + "id": "97f2fee5", "metadata": {}, "outputs": [], "source": [ "#| export\n", - "def patch_to(cls, as_prop=False, cls_method=False, set_prop=False):\n", + "def _strip_patch_name(nm):\n", + " \"Strip trailing `__` from `nm` if it doesn't start with `_`\"\n", + " return nm[:-2] if nm.endswith('__') and not nm.startswith('_') else nm\n", + "\n", + "def patch_to(cls, as_prop=False, cls_method=False, set_prop=False, nm=None):\n", " \"Decorator: add `f` to `cls`\"\n", " if not isinstance(cls, (tuple,list)): cls=(cls,)\n", " def _inner(f):\n", " for c_ in cls:\n", " nf = copy_func(f)\n", - " nm = f.__name__\n", + " fnm = nm or _strip_patch_name(f.__name__)\n", " # `functools.update_wrapper` when passing patched function to `Pipeline`, so we do it manually\n", " for o in functools.WRAPPER_ASSIGNMENTS: setattr(nf, o, getattr(f,o))\n", - " nf.__qualname__ = f\"{c_.__name__}.{nm}\"\n", - " if cls_method: setattr(c_, nm, _clsmethod(nf))\n", + " nf.__name__ = fnm\n", + " nf.__qualname__ = f\"{c_.__name__}.{fnm}\"\n", + " if cls_method: setattr(c_, fnm, _clsmethod(nf))\n", " else:\n", - " if set_prop: setattr(c_, nm, getattr(c_, nm).setter(nf))\n", - " elif as_prop: setattr(c_, nm, property(nf))\n", + " if set_prop: setattr(c_, fnm, getattr(c_, fnm).setter(nf))\n", + " elif as_prop: setattr(c_, fnm, property(nf))\n", " else:\n", - " onm = '_orig_'+nm\n", - " if hasattr(c_, nm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, nm))\n", - " setattr(c_, nm, nf)\n", + " onm = '_orig_'+fnm\n", + " if hasattr(c_, fnm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, fnm))\n", + " setattr(c_, fnm, nf)\n", " # Avoid clobbering existing functions\n", - " return globals().get(nm, builtins.__dict__.get(nm, None))\n", + " return globals().get(fnm, builtins.__dict__.get(fnm, None))\n", " return _inner" ] }, @@ -6215,20 +6220,78 @@ "test_eq(t.func_mult(4), 8)" ] }, + { + "cell_type": "markdown", + "id": "521f1b1e", + "metadata": {}, + "source": [ + "You can also rename the function in the patched class:" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "e4c74c53", + "id": "d50c188d", + "metadata": {}, + "outputs": [], + "source": [ + "class _T8(int): pass \n", + "\n", + "@patch_to(_T8, nm='add_value')\n", + "def func2(self, a): return self+a\n", + "\n", + "t = _T8(1)\n", + "test_eq(t.add_value(2), 3)\n", + "test_eq(_T8.add_value.__name__, 'add_value')\n", + "assert not hasattr(t, 'func2')" + ] + }, + { + "cell_type": "markdown", + "id": "81877f71", + "metadata": {}, + "source": [ + "A `__` suffix is stripped (unless there's also a `_` prefix):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cdd9dedb", + "metadata": {}, + "outputs": [], + "source": [ + "class _T9(int): pass \n", + "\n", + "@patch_to(_T9)\n", + "def func__(self, a): return self+a\n", + "\n", + "t = _T9(1)\n", + "test_eq(t.func(2), 3)\n", + "test_eq(_T9.func.__name__, 'func')\n", + "assert not hasattr(t, 'func__')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8faf7b86", "metadata": {}, "outputs": [], "source": [ "#| export\n", - "def patch(f=None, *, as_prop=False, cls_method=False, set_prop=False):\n", + "def patch(f=None, *, as_prop=False, cls_method=False, set_prop=False, nm=None):\n", " \"Decorator: add `f` to the first parameter's class (based on f's type annotations)\"\n", - " if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop)\n", + " if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop, nm=nm)\n", " ann,glb,loc = get_annotations_ex(f)\n", - " cls = union2tuple(eval_type(ann.pop('cls') if cls_method else next(iter(ann.values())), glb, loc))\n", - " return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop)(f)" + " if cls_method:\n", + " if 'cls' not in ann: raise TypeError(f\"@patch with cls_method=True requires 'cls' to have a type annotation\")\n", + " cls = ann.pop('cls')\n", + " else:\n", + " if not ann: raise TypeError(f\"@patch requires the first parameter of `{f.__name__}` to have a type annotation\")\n", + " cls = next(iter(ann.values()))\n", + " cls = union2tuple(eval_type(cls, glb, loc))\n", + " return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop, nm=nm)(f)" ] }, { @@ -6348,16 +6411,37 @@ { "cell_type": "code", "execution_count": null, - "id": "f05267a8", + "id": "591af803", "metadata": {}, "outputs": [], "source": [ - "#| export\n", - "def patch_property(f):\n", - " \"Deprecated; use `patch(as_prop=True)` instead\"\n", - " warnings.warn(\"`patch_property` is deprecated and will be removed; use `patch(as_prop=True)` instead\")\n", - " cls = next(iter(f.__annotations__.values()))\n", - " return patch_to(cls, as_prop=True)(f)" + "class _T8(int): pass \n", + "\n", + "@patch(nm='add_value')\n", + "def func2(self:_T8, a): return self+a\n", + "\n", + "t = _T8(1)\n", + "test_eq(t.add_value(2), 3)\n", + "test_eq(_T8.add_value.__name__, 'add_value')\n", + "assert not hasattr(t, 'func2')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fdbfff66", + "metadata": {}, + "outputs": [], + "source": [ + "class _T9(int): pass \n", + "\n", + "@patch\n", + "def func__(self:_T9, a): return self+a\n", + "\n", + "t = _T9(1)\n", + "test_eq(t.func(2), 3)\n", + "test_eq(_T9.func.__name__, 'func')\n", + "assert not hasattr(t, 'func__')" ] }, { diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 4ed50ecc..1a1cae10 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -729,18 +729,7 @@ "execution_count": null, "id": "68e52115", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "__main__.L" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "#| export\n", "Sequence.register(L);" @@ -2308,7 +2297,7 @@ "source": [ "#| export\n", "@patch\n", - "def product(self:L):\n", + "def product__(self:L):\n", " \"Product of the items\"\n", " return self.reduce(operator.mul, 1)" ] @@ -3236,19 +3225,13 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L283){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/foundation.py#L577){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.get\n", "\n", "> Config.get (k, default=None)" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L283){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### Config.get\n", - "\n", "> Config.get (k, default=None)" ] }, @@ -3337,7 +3320,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L297){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/foundation.py#L591){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "### Config.find\n", "\n", @@ -3346,12 +3329,6 @@ "*Search `cfg_path` and its parents to find `cfg_name`*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/foundation.py#L297){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "### Config.find\n", - "\n", "> Config.find (cfg_name, cfg_path=None, **kwargs)\n", "\n", "*Search `cfg_path` and its parents to find `cfg_name`*" @@ -3423,7 +3400,13 @@ "source": [] } ], - "metadata": {}, + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index c77da961..eae0006e 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -486,7 +486,7 @@ { "data": { "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAyADIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDi6KKK+ZP3EKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//Z", - "image/png": "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAfUlEQVR4AWL8zzAYAdNgdBTDqLNIAqORSAoYDS1SwGhokQJGQ4sUMBpapIDR0CIFjIYWKWA0tEgBo6FFChgNLVLAaGiRAkZDixQwGlqkgNHQIgWMhhYpYDS0SAGjoUUKGA0tUsBoaJECRkOLFDAaWqSA0dAiBYyGFmCkhBYAQaoBY/NmMJ4AAAAASUVORK5CYII=", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAZ0lEQVR4Ae3SsREAIAzEsMD+O8MM7lKI+ouc8Hmz8d2NR804q/wLLVpFoGy1RasIlK22aBWBstUWrSJQttqiVQTKVlu0ikDZaotWEShbbdEqAmWrLVpFoGy1RasIlK22aBWBstVW0fpBqgFjLbA8fgAAAABJRU5ErkJggg==", "text/plain": [ "" ] @@ -525,7 +525,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAZklEQVR4nM3OMQEAMAyAMIZ/z52B/iUK8oYiSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkSZIkrwO7D0GqAWPcq78HAAAAAElFTkSuQmCC", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAS0lEQVR4nO3OsQEAEADAMPz/Mw9YMjE0F2Tu8aP1OnBXS9QStUQtUUvUErVELVFL1BK1RC1RS9QStUQtUUvUErVELVFL1BK1RC1xAEGqAWOFuDKrAAAAAElFTkSuQmCC", "text/plain": [ "" ] @@ -933,7 +933,7 @@ { "data": { "text/plain": [ - "'pip 25.2 from /Users/jhoward/aai-ws/.venv/lib/python3.12/site-packages/pip (python 3.12)'" + "'pip 25.3 from /Users/jhoward/aai-ws/.venv/lib/python3.12/site-packages/pip (python 3.12)'" ] }, "execution_count": null, @@ -1751,7 +1751,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L413){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L452){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -1760,12 +1760,6 @@ "*Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L413){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### ReindexCollection\n", - "\n", "> ReindexCollection (coll, idxs=None, cache=None, tfm=)\n", "\n", "*Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`*" @@ -1839,7 +1833,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L424){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L463){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -1848,12 +1842,6 @@ "*Replace `self.idxs` with idxs*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L424){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "###### ReindexCollection.reindex\n", - "\n", "> ReindexCollection.reindex (idxs)\n", "\n", "*Replace `self.idxs` with idxs*" @@ -1944,7 +1932,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L428){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L467){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -1953,12 +1941,6 @@ "*Clear LRU cache*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L428){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "##### ReindexCollection.cache_clear\n", - "\n", "> ReindexCollection.cache_clear ()\n", "\n", "*Clear LRU cache*" @@ -2011,7 +1993,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L425){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L464){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -2020,12 +2002,6 @@ "*Randomly shuffle indices*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L425){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "##### ReindexCollection.shuffle\n", - "\n", "> ReindexCollection.shuffle ()\n", "\n", "*Randomly shuffle indices*" @@ -2057,7 +2033,7 @@ { "data": { "text/plain": [ - "['c', 'b', 'e', 'a', 'd', 'h', 'g', 'f']" + "['f', 'c', 'd', 'e', 'h', 'a', 'b', 'g']" ] }, "execution_count": null, @@ -2454,7 +2430,7 @@ { "data": { "text/plain": [ - "'tender-otter-sprints-p753'" + "'vanilla-ray-begins-ymsg'" ] }, "execution_count": null, @@ -2483,7 +2459,7 @@ { "data": { "text/plain": [ - "'kind-sunrise-9bib'" + "'dreamy-key-qr13'" ] }, "execution_count": null, @@ -2512,7 +2488,7 @@ { "data": { "text/plain": [ - "'sweet-signal-goes-loudly-wk347d'" + "'hopeful-rainbow-lifts-lightly-8apkxe'" ] }, "execution_count": null, @@ -2553,10 +2529,15 @@ "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "65,581,614,489,600\n" + "ename": "TypeError", + "evalue": "'NoneType' object is not callable", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[113]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[43mn_friendly_names\u001b[49m\u001b[43m(\u001b[49m\u001b[32;43m4\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m,\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m)\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[112]\u001b[39m\u001b[32m, line 5\u001b[39m, in \u001b[36mn_friendly_names\u001b[39m\u001b[34m(levels, suffix)\u001b[39m\n\u001b[32m 3\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mNumber of possible combos for `friendly_names\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 4\u001b[39m ns = [\u001b[32m102\u001b[39m,\u001b[32m116\u001b[39m,\u001b[32m110\u001b[39m,\u001b[32m30\u001b[39m]\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mproduct\u001b[49m\u001b[43m(\u001b[49m\u001b[43mns\u001b[49m\u001b[43m[\u001b[49m\u001b[43m:\u001b[49m\u001b[43mlevels\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m*\u001b[32m36\u001b[39m**suffix\n", + "\u001b[31mTypeError\u001b[39m: 'NoneType' object is not callable" ] } ], @@ -4500,7 +4481,13 @@ "source": [] } ], - "metadata": {}, + "metadata": { + "kernelspec": { + "display_name": "python3", + "language": "python", + "name": "python3" + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/nbs/03b_net.ipynb b/nbs/03b_net.ipynb index a32d27ff..6c97fab4 100644 --- a/nbs/03b_net.ipynb +++ b/nbs/03b_net.ipynb @@ -191,7 +191,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/net.py#L67){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/net.py#L67){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### HTTP4xxClientError\n", "\n", @@ -200,12 +200,6 @@ "*Base class for client exceptions (code 4xx) from `url*` functions*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/net.py#L67){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### HTTP4xxClientError\n", - "\n", "> HTTP4xxClientError (url, code, msg, hdrs, fp)\n", "\n", "*Base class for client exceptions (code 4xx) from `url*` functions*" @@ -230,7 +224,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/net.py#L72){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/net.py#L72){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### HTTP5xxServerError\n", "\n", @@ -239,12 +233,6 @@ "*Base class for server exceptions (code 5xx) from `url*` functions*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/net.py#L72){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### HTTP5xxServerError\n", - "\n", "> HTTP5xxServerError (url, code, msg, hdrs, fp)\n", "\n", "*Base class for server exceptions (code 5xx) from `url*` functions*" @@ -345,12 +333,11 @@ "text": [ "404 Not Found\n", "====Error Body====\n", - "{\n", - " \"message\": \"Not Found\",\n", - " \"documentation_url\": \"https://docs.github.com/rest\",\n", - " \"status\": \"404\"\n", - "}\n", - "\n" + "{\r\n", + " \"message\": \"Not Found\",\r\n", + " \"documentation_url\": \"https://docs.github.com/rest\",\r\n", + " \"status\": \"404\"\r\n", + "}\n" ] } ], @@ -400,6 +387,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| notest\n", "test_eq(urljson('https://httpbin.org/get')['headers']['User-Agent'], url_default_headers['User-Agent'])" ] }, diff --git a/settings.ini b/settings.ini index e44c3d7c..810cc1cf 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.8.19 +version = 1.9.0 min_python = 3.10 audience = Developers language = English From 760f58491833d868a8f8a8ffd9041ccf25d17143 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 14:01:36 +1000 Subject: [PATCH 164/182] release --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b104eb0a..fb729d27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ +## 1.9.0 + +### Breaking Changes + +- Patching a function that has a name ending in `__` now creates a method with that suffix stripped. Previously it would use the name directly. + +### New Features + +- Handle `nm` in `patch_to` and `patch`, and strip `__` suffix in `patch` ([#715](https://github.com/AnswerDotAI/fastcore/issues/715)) ## 1.8.18 From 723f492af19bbe42b96dfc1ef66f688cbd84729f Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 14:01:49 +1000 Subject: [PATCH 165/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 0a0a43a5..38cf6dbe 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/settings.ini b/settings.ini index 810cc1cf..0c4e843d 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.9.0 +version = 1.9.1 min_python = 3.10 audience = Developers language = English From df6efe2869f64f3d95fd533749bfe21fb0c9a157 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 14:17:05 +1000 Subject: [PATCH 166/182] fixes #716 --- fastcore/_modidx.py | 1 + fastcore/foundation.py | 14 +++++++++++++- nbs/02_foundation.ipynb | 40 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index ba017c5f..d0398b40 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -401,6 +401,7 @@ 'fastcore.foundation.L.zipwith': ('foundation.html#l.zipwith', 'fastcore/foundation.py'), 'fastcore.foundation._L_Meta': ('foundation.html#_l_meta', 'fastcore/foundation.py'), 'fastcore.foundation._L_Meta.__call__': ('foundation.html#_l_meta.__call__', 'fastcore/foundation.py'), + 'fastcore.foundation._batched': ('foundation.html#_batched', 'fastcore/foundation.py'), 'fastcore.foundation._f': ('foundation.html#_f', 'fastcore/foundation.py'), 'fastcore.foundation.add_docs': ('foundation.html#add_docs', 'fastcore/foundation.py'), 'fastcore.foundation.coll_repr': ('foundation.html#coll_repr', 'fastcore/foundation.py'), diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 35673552..54a38507 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -479,11 +479,23 @@ def pairwise(self:L): "Same as `itertools.pairwise`" return self._new(itertools.pairwise(self)) +# %% ../nbs/02_foundation.ipynb +def _batched(iterable, n): + "Batch data into tuples of length n. The last batch may be shorter." + if n < 1: raise ValueError('n must be at least one') + it = iter(iterable) + while batch := tuple(itertools.islice(it, n)): + yield batch + +# %% ../nbs/02_foundation.ipynb +try: from itertools import batched +except ImportError: batched = _batched + # %% ../nbs/02_foundation.ipynb @patch def batched(self:L, n): "Same as `itertools.batched`" - return self._new(itertools.batched(self, n)) + return self._new(batched(self, n)) # %% ../nbs/02_foundation.ipynb @patch diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 1a1cae10..e8c0a76a 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -2672,7 +2672,35 @@ { "cell_type": "code", "execution_count": null, - "id": "c5984ee1", + "id": "1f2a370c", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def _batched(iterable, n):\n", + " \"Batch data into tuples of length n. The last batch may be shorter.\"\n", + " if n < 1: raise ValueError('n must be at least one')\n", + " it = iter(iterable)\n", + " while batch := tuple(itertools.islice(it, n)):\n", + " yield batch" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41aefe58", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "try: from itertools import batched\n", + "except ImportError: batched = _batched" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9f7ffb9", "metadata": {}, "outputs": [], "source": [ @@ -2680,7 +2708,7 @@ "@patch\n", "def batched(self:L, n):\n", " \"Same as `itertools.batched`\"\n", - " return self._new(itertools.batched(self, n))" + " return self._new(batched(self, n))" ] }, { @@ -2952,6 +2980,14 @@ "test_eq(L([1,2,3]).flatten(), [1,2,3]) # already flat" ] }, + { + "cell_type": "markdown", + "id": "88fa0ef8", + "metadata": {}, + "source": [ + "##### User" + ] + }, { "cell_type": "markdown", "id": "8cf19e29", From 6e263f2552ad1596e05a73d6387b2437e55faf30 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 14:21:43 +1000 Subject: [PATCH 167/182] rename --- fastcore/_modidx.py | 2 +- fastcore/foundation.py | 2 +- nbs/02_foundation.ipynb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index d0398b40..452b11dd 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -346,7 +346,7 @@ 'fastcore.foundation.L.argfirst': ('foundation.html#l.argfirst', 'fastcore/foundation.py'), 'fastcore.foundation.L.argwhere': ('foundation.html#l.argwhere', 'fastcore/foundation.py'), 'fastcore.foundation.L.attrgot': ('foundation.html#l.attrgot', 'fastcore/foundation.py'), - 'fastcore.foundation.L.batched': ('foundation.html#l.batched', 'fastcore/foundation.py'), + 'fastcore.foundation.L.batched__': ('foundation.html#l.batched__', 'fastcore/foundation.py'), 'fastcore.foundation.L.combinations': ('foundation.html#l.combinations', 'fastcore/foundation.py'), 'fastcore.foundation.L.compress': ('foundation.html#l.compress', 'fastcore/foundation.py'), 'fastcore.foundation.L.concat': ('foundation.html#l.concat', 'fastcore/foundation.py'), diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 54a38507..7fa59a32 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -493,7 +493,7 @@ def _batched(iterable, n): # %% ../nbs/02_foundation.ipynb @patch -def batched(self:L, n): +def batched__(self:L, n): "Same as `itertools.batched`" return self._new(batched(self, n)) diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index e8c0a76a..6ee34de0 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -2706,7 +2706,7 @@ "source": [ "#| export\n", "@patch\n", - "def batched(self:L, n):\n", + "def batched__(self:L, n):\n", " \"Same as `itertools.batched`\"\n", " return self._new(batched(self, n))" ] From d09b4c662a11a82fd45c676be794c203cefe9297 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 14:22:18 +1000 Subject: [PATCH 168/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb729d27..a7b4b188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.9.1 + +### New Features + +- Add pre-3.11 support for `batched` ([#716](https://github.com/AnswerDotAI/fastcore/issues/716)) + + ## 1.9.0 ### Breaking Changes From 1f05a59b49dbf5329df006a514b17119c8c8bd8c Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 14:22:35 +1000 Subject: [PATCH 169/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 38cf6dbe..2cbc28c3 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.9.1" +__version__ = "1.9.2" diff --git a/settings.ini b/settings.ini index 0c4e843d..ce06770c 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.9.1 +version = 1.9.2 min_python = 3.10 audience = Developers language = English From 587642bd69dc3af5bcef7ac4017af241d46e1d58 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 14:24:59 +1000 Subject: [PATCH 170/182] skip test --- nbs/03b_net.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbs/03b_net.ipynb b/nbs/03b_net.ipynb index 6c97fab4..e3d6b200 100644 --- a/nbs/03b_net.ipynb +++ b/nbs/03b_net.ipynb @@ -387,7 +387,7 @@ "metadata": {}, "outputs": [], "source": [ - "#| notest\n", + "#| eval: false\n", "test_eq(urljson('https://httpbin.org/get')['headers']['User-Agent'], url_default_headers['User-Agent'])" ] }, From f1dac487baaf3a128ed59f68e6f0d3b07e7103b8 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 17:43:22 +1000 Subject: [PATCH 171/182] fixes #717 --- fastcore/_modidx.py | 5 +- fastcore/basics.py | 36 ++++++-------- fastcore/foundation.py | 4 +- nbs/01_basics.ipynb | 108 +++++++++++++++------------------------- nbs/02_foundation.ipynb | 12 +---- 5 files changed, 62 insertions(+), 103 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 452b11dd..bf808baf 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -100,7 +100,6 @@ 'fastcore.basics._oper': ('basics.html#_oper', 'fastcore/basics.py'), 'fastcore.basics._risinstance': ('basics.html#_risinstance', 'fastcore/basics.py'), 'fastcore.basics._store_attr': ('basics.html#_store_attr', 'fastcore/basics.py'), - 'fastcore.basics._strip_patch_name': ('basics.html#_strip_patch_name', 'fastcore/basics.py'), 'fastcore.basics._typeerr': ('basics.html#_typeerr', 'fastcore/basics.py'), 'fastcore.basics._using_attr': ('basics.html#_using_attr', 'fastcore/basics.py'), 'fastcore.basics.add_props': ('basics.html#add_props', 'fastcore/basics.py'), @@ -346,7 +345,7 @@ 'fastcore.foundation.L.argfirst': ('foundation.html#l.argfirst', 'fastcore/foundation.py'), 'fastcore.foundation.L.argwhere': ('foundation.html#l.argwhere', 'fastcore/foundation.py'), 'fastcore.foundation.L.attrgot': ('foundation.html#l.attrgot', 'fastcore/foundation.py'), - 'fastcore.foundation.L.batched__': ('foundation.html#l.batched__', 'fastcore/foundation.py'), + 'fastcore.foundation.L.batched': ('foundation.html#l.batched', 'fastcore/foundation.py'), 'fastcore.foundation.L.combinations': ('foundation.html#l.combinations', 'fastcore/foundation.py'), 'fastcore.foundation.L.compress': ('foundation.html#l.compress', 'fastcore/foundation.py'), 'fastcore.foundation.L.concat': ('foundation.html#l.concat', 'fastcore/foundation.py'), @@ -366,7 +365,7 @@ 'fastcore.foundation.L.pairwise': ('foundation.html#l.pairwise', 'fastcore/foundation.py'), 'fastcore.foundation.L.partition': ('foundation.html#l.partition', 'fastcore/foundation.py'), 'fastcore.foundation.L.permutations': ('foundation.html#l.permutations', 'fastcore/foundation.py'), - 'fastcore.foundation.L.product__': ('foundation.html#l.product__', 'fastcore/foundation.py'), + 'fastcore.foundation.L.product': ('foundation.html#l.product', 'fastcore/foundation.py'), 'fastcore.foundation.L.range': ('foundation.html#l.range', 'fastcore/foundation.py'), 'fastcore.foundation.L.reduce': ('foundation.html#l.reduce', 'fastcore/foundation.py'), 'fastcore.foundation.L.renumerate': ('foundation.html#l.renumerate', 'fastcore/foundation.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index 7f1be768..5fd60cd6 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -1051,31 +1051,25 @@ def __init__(self, f): self.f = f def __get__(self, _, f_cls): return MethodType(self.f, f_cls) # %% ../nbs/01_basics.ipynb -def _strip_patch_name(nm): - "Strip trailing `__` from `nm` if it doesn't start with `_`" - return nm[:-2] if nm.endswith('__') and not nm.startswith('_') else nm - -def patch_to(cls, as_prop=False, cls_method=False, set_prop=False, nm=None): +def patch_to(cls, as_prop=False, cls_method=False, set_prop=False, nm=None, glb=None): "Decorator: add `f` to `cls`" - if not isinstance(cls, (tuple,list)): cls=(cls,) + if glb is None: glb = sys._getframe(1).f_globals def _inner(f): - for c_ in cls: + _nm = nm or f.__name__ + for c_ in tuplify(cls): nf = copy_func(f) - fnm = nm or _strip_patch_name(f.__name__) - # `functools.update_wrapper` when passing patched function to `Pipeline`, so we do it manually for o in functools.WRAPPER_ASSIGNMENTS: setattr(nf, o, getattr(f,o)) - nf.__name__ = fnm - nf.__qualname__ = f"{c_.__name__}.{fnm}" - if cls_method: setattr(c_, fnm, _clsmethod(nf)) + nf.__name__ = _nm + nf.__qualname__ = f"{c_.__name__}.{_nm}" + if cls_method: attr = _clsmethod(nf) + elif set_prop: attr = getattr(c_, _nm).setter(nf) + elif as_prop: attr = property(nf) else: - if set_prop: setattr(c_, fnm, getattr(c_, fnm).setter(nf)) - elif as_prop: setattr(c_, fnm, property(nf)) - else: - onm = '_orig_'+fnm - if hasattr(c_, fnm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, fnm)) - setattr(c_, fnm, nf) - # Avoid clobbering existing functions - return globals().get(fnm, builtins.__dict__.get(fnm, None)) + onm = '_orig_'+_nm + if hasattr(c_, _nm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, _nm)) + attr = nf + setattr(c_, _nm, attr) + return glb.get(_nm, builtins.__dict__.get(_nm, None)) return _inner # %% ../nbs/01_basics.ipynb @@ -1090,7 +1084,7 @@ def patch(f=None, *, as_prop=False, cls_method=False, set_prop=False, nm=None): if not ann: raise TypeError(f"@patch requires the first parameter of `{f.__name__}` to have a type annotation") cls = next(iter(ann.values())) cls = union2tuple(eval_type(cls, glb, loc)) - return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop, nm=nm)(f) + return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop, nm=nm, glb=sys._getframe(1).f_globals)(f) # %% ../nbs/01_basics.ipynb def compile_re(pat): diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 7fa59a32..66405bf7 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -409,7 +409,7 @@ def sum(self:L): # %% ../nbs/02_foundation.ipynb @patch -def product__(self:L): +def product(self:L): "Product of the items" return self.reduce(operator.mul, 1) @@ -493,7 +493,7 @@ def _batched(iterable, n): # %% ../nbs/02_foundation.ipynb @patch -def batched__(self:L, n): +def batched(self:L, n): "Same as `itertools.batched`" return self._new(batched(self, n)) diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index b6a9f37a..e579f42e 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -1215,7 +1215,31 @@ "execution_count": null, "id": "da11467b", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/markdown": [ + "---\n", + "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L198){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "### Inf\n", + "\n", + "> Inf ()\n", + "\n", + "*Infinite lists*" + ], + "text/plain": [ + "> Inf ()\n", + "\n", + "*Infinite lists*" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "show_doc(Inf);" ] @@ -6035,36 +6059,30 @@ { "cell_type": "code", "execution_count": null, - "id": "97f2fee5", + "id": "3f2733ef", "metadata": {}, "outputs": [], "source": [ "#| export\n", - "def _strip_patch_name(nm):\n", - " \"Strip trailing `__` from `nm` if it doesn't start with `_`\"\n", - " return nm[:-2] if nm.endswith('__') and not nm.startswith('_') else nm\n", - "\n", - "def patch_to(cls, as_prop=False, cls_method=False, set_prop=False, nm=None):\n", + "def patch_to(cls, as_prop=False, cls_method=False, set_prop=False, nm=None, glb=None):\n", " \"Decorator: add `f` to `cls`\"\n", - " if not isinstance(cls, (tuple,list)): cls=(cls,)\n", + " if glb is None: glb = sys._getframe(1).f_globals\n", " def _inner(f):\n", - " for c_ in cls:\n", + " _nm = nm or f.__name__\n", + " for c_ in tuplify(cls):\n", " nf = copy_func(f)\n", - " fnm = nm or _strip_patch_name(f.__name__)\n", - " # `functools.update_wrapper` when passing patched function to `Pipeline`, so we do it manually\n", " for o in functools.WRAPPER_ASSIGNMENTS: setattr(nf, o, getattr(f,o))\n", - " nf.__name__ = fnm\n", - " nf.__qualname__ = f\"{c_.__name__}.{fnm}\"\n", - " if cls_method: setattr(c_, fnm, _clsmethod(nf))\n", + " nf.__name__ = _nm\n", + " nf.__qualname__ = f\"{c_.__name__}.{_nm}\"\n", + " if cls_method: attr = _clsmethod(nf)\n", + " elif set_prop: attr = getattr(c_, _nm).setter(nf)\n", + " elif as_prop: attr = property(nf)\n", " else:\n", - " if set_prop: setattr(c_, fnm, getattr(c_, fnm).setter(nf))\n", - " elif as_prop: setattr(c_, fnm, property(nf))\n", - " else:\n", - " onm = '_orig_'+fnm\n", - " if hasattr(c_, fnm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, fnm))\n", - " setattr(c_, fnm, nf)\n", - " # Avoid clobbering existing functions\n", - " return globals().get(fnm, builtins.__dict__.get(fnm, None))\n", + " onm = '_orig_'+_nm\n", + " if hasattr(c_, _nm) and not hasattr(c_, onm): setattr(c_, onm, getattr(c_, _nm))\n", + " attr = nf\n", + " setattr(c_, _nm, attr)\n", + " return glb.get(_nm, builtins.__dict__.get(_nm, None))\n", " return _inner" ] }, @@ -6246,32 +6264,6 @@ "assert not hasattr(t, 'func2')" ] }, - { - "cell_type": "markdown", - "id": "81877f71", - "metadata": {}, - "source": [ - "A `__` suffix is stripped (unless there's also a `_` prefix):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cdd9dedb", - "metadata": {}, - "outputs": [], - "source": [ - "class _T9(int): pass \n", - "\n", - "@patch_to(_T9)\n", - "def func__(self, a): return self+a\n", - "\n", - "t = _T9(1)\n", - "test_eq(t.func(2), 3)\n", - "test_eq(_T9.func.__name__, 'func')\n", - "assert not hasattr(t, 'func__')" - ] - }, { "cell_type": "code", "execution_count": null, @@ -6291,7 +6283,7 @@ " if not ann: raise TypeError(f\"@patch requires the first parameter of `{f.__name__}` to have a type annotation\")\n", " cls = next(iter(ann.values()))\n", " cls = union2tuple(eval_type(cls, glb, loc))\n", - " return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop, nm=nm)(f)" + " return patch_to(cls, as_prop=as_prop, cls_method=cls_method, set_prop=set_prop, nm=nm, glb=sys._getframe(1).f_globals)(f)" ] }, { @@ -6426,24 +6418,6 @@ "assert not hasattr(t, 'func2')" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "fdbfff66", - "metadata": {}, - "outputs": [], - "source": [ - "class _T9(int): pass \n", - "\n", - "@patch\n", - "def func__(self:_T9, a): return self+a\n", - "\n", - "t = _T9(1)\n", - "test_eq(t.func(2), 3)\n", - "test_eq(_T9.func.__name__, 'func')\n", - "assert not hasattr(t, 'func__')" - ] - }, { "cell_type": "markdown", "id": "d9310d77", diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 6ee34de0..18c626a9 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -2297,7 +2297,7 @@ "source": [ "#| export\n", "@patch\n", - "def product__(self:L):\n", + "def product(self:L):\n", " \"Product of the items\"\n", " return self.reduce(operator.mul, 1)" ] @@ -2706,7 +2706,7 @@ "source": [ "#| export\n", "@patch\n", - "def batched__(self:L, n):\n", + "def batched(self:L, n):\n", " \"Same as `itertools.batched`\"\n", " return self._new(batched(self, n))" ] @@ -2980,14 +2980,6 @@ "test_eq(L([1,2,3]).flatten(), [1,2,3]) # already flat" ] }, - { - "cell_type": "markdown", - "id": "88fa0ef8", - "metadata": {}, - "source": [ - "##### User" - ] - }, { "cell_type": "markdown", "id": "8cf19e29", From bb7a152068d049ccc301f839f542aa6e45003dfb Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 17:44:46 +1000 Subject: [PATCH 172/182] release --- CHANGELOG.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b4b188..7b350d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,22 +2,17 @@ -## 1.9.1 - -### New Features - -- Add pre-3.11 support for `batched` ([#716](https://github.com/AnswerDotAI/fastcore/issues/716)) - - -## 1.9.0 +## 1.9.2 ### Breaking Changes -- Patching a function that has a name ending in `__` now creates a method with that suffix stripped. Previously it would use the name directly. +- Patching a function no long clobbers a function with the same name in the current module ### New Features -- Handle `nm` in `patch_to` and `patch`, and strip `__` suffix in `patch` ([#715](https://github.com/AnswerDotAI/fastcore/issues/715)) +- Have `@patch` not overwrite existing functions ([#717](https://github.com/AnswerDotAI/fastcore/issues/717)) +- Add pre-3.11 support for `batched` ([#716](https://github.com/AnswerDotAI/fastcore/issues/716)) +- Handle `nm` in `patch_to` and `patch` ([#715](https://github.com/AnswerDotAI/fastcore/issues/715)) ## 1.8.18 From 3c2026a3f24551305d03e84ecd7319b3468573fa Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 13 Dec 2025 17:45:02 +1000 Subject: [PATCH 173/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 2cbc28c3..bdc0f162 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.9.2" +__version__ = "1.9.3" diff --git a/settings.ini b/settings.ini index ce06770c..73af8a65 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.9.2 +version = 1.9.3 min_python = 3.10 audience = Developers language = English From c9338c22446b4ca95ad378571278f7d919856be5 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sun, 14 Dec 2025 05:02:04 +1000 Subject: [PATCH 174/182] comment --- nbs/12_tools.ipynb | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/nbs/12_tools.ipynb b/nbs/12_tools.ipynb index c3de3139..21be7125 100644 --- a/nbs/12_tools.ipynb +++ b/nbs/12_tools.ipynb @@ -709,9 +709,11 @@ "outputs": [], "source": [ "#| hide\n", - "for f,o in list(globals().items()):\n", - " if callable(o) and hasattr(o, '__module__') and o.__module__ == '__main__' and not f.startswith(('_', 'receive_')):\n", - " assert f == get_schema(globals()[f])['name']" + "# Verify that all public functions defined in this module have valid schemas\n", + "# (i.e., they have proper type annotations and docstrings required by get_schema)\n", + "for f,_o in list(globals().items()):\n", + " if callable(_o) and hasattr(_o, '__module__') and _o.__module__ == '__main__' and not f.startswith(('_', 'receive_')):\n", + " test_eq(f, get_schema(globals()[f])['name'])" ] }, { @@ -734,14 +736,6 @@ "from nbdev import nbdev_export\n", "nbdev_export()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "874f949c", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": {}, From 6981b20c75dafb1f905e8d046c8c53b71b28948f Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 19 Dec 2025 11:47:42 +1000 Subject: [PATCH 175/182] fixes #718 --- fastcore/_modidx.py | 1 + fastcore/xtras.py | 21 +++++++---- nbs/03_xtras.ipynb | 88 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 6 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index bf808baf..c47571c6 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -739,6 +739,7 @@ 'fastcore.xtras.trim_wraps': ('xtras.html#trim_wraps', 'fastcore/xtras.py'), 'fastcore.xtras.truncstr': ('xtras.html#truncstr', 'fastcore/xtras.py'), 'fastcore.xtras.type2str': ('xtras.html#type2str', 'fastcore/xtras.py'), + 'fastcore.xtras.unqid': ('xtras.html#unqid', 'fastcore/xtras.py'), 'fastcore.xtras.untar_dir': ('xtras.html#untar_dir', 'fastcore/xtras.py'), 'fastcore.xtras.utc2local': ('xtras.html#utc2local', 'fastcore/xtras.py'), 'fastcore.xtras.vars_pub': ('xtras.html#vars_pub', 'fastcore/xtras.py'), diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 12f21f07..05580bfc 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -10,12 +10,12 @@ 'bunzip', 'loads', 'loads_multi', 'dumps', 'untar_dir', 'repo_details', 'shell', 'ssh', 'rsync_multi', 'run', 'open_file', 'save_pickle', 'load_pickle', 'parse_env', 'expand_wildcards', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'mapped', 'IterLen', 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', - 'asave_iter', 'friendly_name', 'n_friendly_names', 'exec_eval', 'get_source_link', 'truncstr', 'sparkline', - 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', - 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', 'modified_env', - 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', 'Unset', 'nullable_dc', - 'make_nullable', 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict', 'is_namedtuple', 'CachedIter', - 'CachedAwaitable', 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] + 'asave_iter', 'unqid', 'friendly_name', 'n_friendly_names', 'exec_eval', 'get_source_link', 'truncstr', + 'sparkline', 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', + 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'utc2local', 'local2utc', 'trace', + 'modified_env', 'ContextManagers', 'shufflish', 'console_help', 'hl_md', 'type2str', 'dataclass_src', + 'Unset', 'nullable_dc', 'make_nullable', 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict', 'is_namedtuple', + 'CachedIter', 'CachedAwaitable', 'reawaitable', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] # %% ../nbs/03_xtras.ipynb from .imports import * @@ -511,6 +511,15 @@ def asave_iter(g): def _(*args, **kwargs): return _save_iter(g, *args, **kwargs) return _ +# %% ../nbs/03_xtras.ipynb +def unqid(seeded=False): + "Generate a unique id suitable for use as a Python identifier" + from base64 import b64encode + from uuid import uuid4,UUID + id4 = UUID(int=random.getrandbits(128), version=4) if seeded else uuid4() + res = b64encode(id4.bytes) + return '_' + res.decode().rstrip('=').translate(str.maketrans('+/', '_-')) + # %% ../nbs/03_xtras.ipynb def friendly_name(levels=3, suffix=4): "Generate a random human-readable name with customizable word levels and suffix length" diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index eae0006e..aa8595d7 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -2393,6 +2393,94 @@ "## Other Helpers" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "35368de5", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def unqid(seeded=False):\n", + " \"Generate a unique id suitable for use as a Python identifier\"\n", + " from base64 import b64encode\n", + " from uuid import uuid4,UUID\n", + " id4 = UUID(int=random.getrandbits(128), version=4) if seeded else uuid4()\n", + " res = b64encode(id4.bytes)\n", + " return '_' + res.decode().rstrip('=').translate(str.maketrans('+/', '_-'))" + ] + }, + { + "cell_type": "markdown", + "id": "d193424a", + "metadata": {}, + "source": [ + "`unqid` generates a random unique identifier that is safe to use as a Python variable name (starts with `_`, uses only alphanumeric characters and underscores). It's based on UUID4, encoded in URL-safe base64.\n", + "\n", + "If `seeded=True`, uses `random.getrandbits` which respects `random.seed()`, making it reproducible. Otherwise uses `uuid4()` which is always random." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e065069", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'_XZ1jJo-rS4yr1Qz6prx-fg'" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "unqid()" + ] + }, + { + "cell_type": "markdown", + "id": "662f49fa", + "metadata": {}, + "source": [ + "With seeding for reproducibility:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b37f4eed", + "metadata": {}, + "outputs": [], + "source": [ + "random.seed(42)\n", + "a = unqid(seeded=True)\n", + "random.seed(42)\n", + "b = unqid(seeded=True)\n", + "test_eq(a, b)" + ] + }, + { + "cell_type": "markdown", + "id": "db2843d8", + "metadata": {}, + "source": [ + "Without seeding - always unique:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "355ab870", + "metadata": {}, + "outputs": [], + "source": [ + "test_ne(unqid(), unqid())" + ] + }, { "cell_type": "code", "execution_count": null, From e4e1b8d2559e501564ddc850bba31bfb6b787253 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 19 Dec 2025 11:48:27 +1000 Subject: [PATCH 176/182] release --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b350d52..ca281a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ +## 1.9.3 + +### New Features + +- Add `unqid` ([#718](https://github.com/AnswerDotAI/fastcore/issues/718)) + + ## 1.9.2 ### Breaking Changes From 23027252677d7b513ead4b421054478fa6d7692d Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Fri, 19 Dec 2025 11:48:41 +1000 Subject: [PATCH 177/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index bdc0f162..84452039 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.9.3" +__version__ = "1.9.4" diff --git a/settings.ini b/settings.ini index 73af8a65..b991843a 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.9.3 +version = 1.9.4 min_python = 3.10 audience = Developers language = English From 299295ccd26bafad449052040a6c98c26647759c Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 20 Dec 2025 08:51:21 +1000 Subject: [PATCH 178/182] fixes #719 --- fastcore/xtras.py | 4 ++-- nbs/01_basics.ipynb | 56 ++++++++++++++------------------------------- nbs/03_xtras.ipynb | 4 ++-- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/fastcore/xtras.py b/fastcore/xtras.py index 05580bfc..955b2ebc 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -251,7 +251,7 @@ def rsync_multi(ip, files, user='ubuntu', persist='5m'): for src,dst in files: shell(f'rsync -az -e "ssh -S {sock}" {src} {user}@{ip}:{dst}') # %% ../nbs/03_xtras.ipynb -def run(cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, stderr=False): +def run(cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, stderr=True): "Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails" # Even the command is same on Windows, we have to add `cmd /c `" import subprocess @@ -271,7 +271,7 @@ def run(cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, stderr=F if stderr and res.stderr: stdout += b' ;; ' + res.stderr if not as_bytes: stdout = stdout.decode().strip() if ignore_ex: return (res.returncode, stdout) - if res.returncode: raise IOError(stdout) + if res.returncode: raise IOError((res.stdout + b' ;; ' + res.stderr).decode().strip()) return stdout # %% ../nbs/03_xtras.ipynb diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index e579f42e..246ae77b 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -6506,7 +6506,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1097){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1095){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ImportEnum\n", "\n", @@ -6568,7 +6568,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1105){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1103){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### StrEnum\n", "\n", @@ -6630,7 +6630,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1115){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1113){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ValEnum\n", "\n", @@ -6716,7 +6716,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1120){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1118){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### Stateful\n", "\n", @@ -6883,7 +6883,7 @@ "text/markdown": [ "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1140){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/basics.py#L1156){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### PrettyString\n", "\n", @@ -6892,12 +6892,6 @@ "*Little hack to get strings to show properly in Jupyter.*" ], "text/plain": [ - "---\n", - "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/master/fastcore/basics.py#L1140){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", - "\n", - "#### PrettyString\n", - "\n", "\n", "\n", "*Little hack to get strings to show properly in Jupyter.*" @@ -7027,7 +7021,7 @@ { "data": { "text/plain": [ - "22" + "16" ] }, "execution_count": null, @@ -7588,10 +7582,6 @@ "*Same as `get_ipython` but returns `False` if not in IPython*" ], "text/plain": [ - "---\n", - "\n", - "### ipython_shell\n", - "\n", "> ipython_shell ()\n", "\n", "*Same as `get_ipython` but returns `False` if not in IPython*" @@ -7624,10 +7614,6 @@ "*Check if code is running in some kind of IPython environment*" ], "text/plain": [ - "---\n", - "\n", - "### in_ipython\n", - "\n", "> in_ipython ()\n", "\n", "*Check if code is running in some kind of IPython environment*" @@ -7660,10 +7646,6 @@ "*Check if the code is running in Google Colaboratory*" ], "text/plain": [ - "---\n", - "\n", - "### in_colab\n", - "\n", "> in_colab ()\n", "\n", "*Check if the code is running in Google Colaboratory*" @@ -7696,10 +7678,6 @@ "*Check if the code is running in a jupyter notebook*" ], "text/plain": [ - "---\n", - "\n", - "### in_jupyter\n", - "\n", "> in_jupyter ()\n", "\n", "*Check if the code is running in a jupyter notebook*" @@ -7732,10 +7710,6 @@ "*Check if the code is running in a jupyter notebook*" ], "text/plain": [ - "---\n", - "\n", - "### in_notebook\n", - "\n", "> in_notebook ()\n", "\n", "*Check if the code is running in a jupyter notebook*" @@ -7764,6 +7738,16 @@ "id": "55db8771", "metadata": {}, "outputs": [ + { + "data": { + "text/plain": [ + "(True, True, False, True)" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + }, { "data": { "text/plain": [ @@ -7807,13 +7791,7 @@ "source": [] } ], - "metadata": { - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } - }, + "metadata": {}, "nbformat": 4, "nbformat_minor": 5 } diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index aa8595d7..2e5c3323 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -892,7 +892,7 @@ "outputs": [], "source": [ "#| export\n", - "def run(cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, stderr=False):\n", + "def run(cmd, *rest, same_in_win=False, ignore_ex=False, as_bytes=False, stderr=True):\n", " \"Pass `cmd` (splitting with `shlex` if string) to `subprocess.run`; return `stdout`; raise `IOError` if fails\"\n", " # Even the command is same on Windows, we have to add `cmd /c `\"\n", " import subprocess\n", @@ -912,7 +912,7 @@ " if stderr and res.stderr: stdout += b' ;; ' + res.stderr\n", " if not as_bytes: stdout = stdout.decode().strip()\n", " if ignore_ex: return (res.returncode, stdout)\n", - " if res.returncode: raise IOError(stdout)\n", + " if res.returncode: raise IOError((res.stdout + b' ;; ' + res.stderr).decode().strip())\n", " return stdout" ] }, From 93c57594a3948180f4b18eec56c6fb5fcc339e58 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 20 Dec 2025 14:13:17 +1000 Subject: [PATCH 179/182] fixes #720 --- fastcore/_modidx.py | 3 + fastcore/foundation.py | 189 ++-- nbs/02_foundation.ipynb | 2074 ++++++++++++++++++++++++++++++--------- 3 files changed, 1712 insertions(+), 554 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index c47571c6..d99e0ab1 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -404,14 +404,17 @@ 'fastcore.foundation._f': ('foundation.html#_f', 'fastcore/foundation.py'), 'fastcore.foundation.add_docs': ('foundation.html#add_docs', 'fastcore/foundation.py'), 'fastcore.foundation.coll_repr': ('foundation.html#coll_repr', 'fastcore/foundation.py'), + 'fastcore.foundation.curryable': ('foundation.html#curryable', 'fastcore/foundation.py'), 'fastcore.foundation.cycle': ('foundation.html#cycle', 'fastcore/foundation.py'), 'fastcore.foundation.docs': ('foundation.html#docs', 'fastcore/foundation.py'), 'fastcore.foundation.is_bool': ('foundation.html#is_bool', 'fastcore/foundation.py'), 'fastcore.foundation.is_indexer': ('foundation.html#is_indexer', 'fastcore/foundation.py'), + 'fastcore.foundation.linesplitter': ('foundation.html#linesplitter', 'fastcore/foundation.py'), 'fastcore.foundation.mask2idxs': ('foundation.html#mask2idxs', 'fastcore/foundation.py'), 'fastcore.foundation.product': ('foundation.html#product', 'fastcore/foundation.py'), 'fastcore.foundation.read_config_file': ('foundation.html#read_config_file', 'fastcore/foundation.py'), 'fastcore.foundation.save_config_file': ('foundation.html#save_config_file', 'fastcore/foundation.py'), + 'fastcore.foundation.splitter': ('foundation.html#splitter', 'fastcore/foundation.py'), 'fastcore.foundation.working_directory': ( 'foundation.html#working_directory', 'fastcore/foundation.py'), 'fastcore.foundation.zip_cycle': ('foundation.html#zip_cycle', 'fastcore/foundation.py')}, diff --git a/fastcore/foundation.py b/fastcore/foundation.py index 66405bf7..35017381 100644 --- a/fastcore/foundation.py +++ b/fastcore/foundation.py @@ -4,12 +4,13 @@ # %% auto 0 __all__ = ['working_directory', 'add_docs', 'docs', 'coll_repr', 'is_bool', 'mask2idxs', 'cycle', 'zip_cycle', 'is_indexer', - 'product', 'CollBase', 'L', 'save_config_file', 'read_config_file', 'Config'] + 'product', 'CollBase', 'L', 'curryable', 'splitter', 'linesplitter', 'save_config_file', 'read_config_file', + 'Config'] # %% ../nbs/02_foundation.ipynb from .imports import * from .basics import * -from functools import lru_cache +from functools import lru_cache,wraps from contextlib import contextmanager from copy import copy from configparser import ConfigParser @@ -45,7 +46,7 @@ def docs(cls): return cls # %% ../nbs/02_foundation.ipynb -def coll_repr(c, max_n=20): +def coll_repr(c, max_n=250): "String repr of up to `max_n` items of (possibly lazy) collection `c`" return f'(#{len(c)}) [' + ','.join(itertools.islice(map(repr,c), max_n)) + ( '...' if len(c)>max_n else '') + ']' @@ -178,6 +179,24 @@ def val2idx(self:L): "Dict from value to index" return val2idx(self) +# %% ../nbs/02_foundation.ipynb +@patch(cls_method=True) +def range(cls:L, a, b=None, step=None): + "Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`" + return cls(range_of(a, b=b, step=step)) + +# %% ../nbs/02_foundation.ipynb +@patch +def enumerate(self:L): + "Same as `enumerate`" + return L(enumerate(self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def renumerate(self:L): + "Same as `renumerate`" + return L(renumerate(self)) + # %% ../nbs/02_foundation.ipynb @patch(cls_method=True) def split(cls:L, s, sep=None, maxsplit=-1): @@ -190,20 +209,92 @@ def splitlines(cls:L, s, keepends=False): "Class Method: Same as `str.splitlines`, but returns an `L`" return cls(s.splitlines(keepends)) +# %% ../nbs/02_foundation.ipynb +def curryable(f): + @wraps(f) + def wrapper(self, *args, **kwargs): + if not isinstance(self, L): return lambda items: f(L(items), self, *args, **kwargs) + return f(L(self), *args, **kwargs) + return wrapper + # %% ../nbs/02_foundation.ipynb @patch +@curryable +def map(self:L, f, *args, **kwargs): + "Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`" + return self._new(map_ex(self, f, *args, gen=False, **kwargs)) + +# %% ../nbs/02_foundation.ipynb +def splitter(sep=None, maxsplit=-1): + "Create a partial function that splits strings into `L`" + return partial(L.split, sep=sep, maxsplit=maxsplit) + +# %% ../nbs/02_foundation.ipynb +def linesplitter(keepends=False): + "Create a partial function that splits strings by lines into `L`" + return partial(L.splitlines, keepends=keepends) + +# %% ../nbs/02_foundation.ipynb +@patch +@curryable def groupby(self:L, key, val=noop): "Same as `fastcore.basics.groupby`" return groupby(self, key, val=val) # %% ../nbs/02_foundation.ipynb @patch +@curryable +def starmap(self:L, f, *args, **kwargs): + "Like `map`, but use `itertools.starmap`" + return self._new(itertools.starmap(partial(f,*args,**kwargs), self)) + +# %% ../nbs/02_foundation.ipynb +@patch +@curryable +def rstarmap(self:L, f, *args, **kwargs): + "Like `starmap`, but reverse the order of args" + return self._new(itertools.starmap(lambda *x: f(*x[::-1], *args, **kwargs), self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def map_dict(self:L, f=noop, *args, **kwargs): + "Like `map`, but creates a dict from `items` to function results" + return {k:f(k, *args,**kwargs) for k in self} + +# %% ../nbs/02_foundation.ipynb +@patch +def zip(self:L, cycled=False): + "Create new `L` with `zip(*items)`" + return self._new((zip_cycle if cycled else zip)(*self)) + +# %% ../nbs/02_foundation.ipynb +@patch +def map_zip(self:L, f, *args, cycled=False, **kwargs): + "Combine `zip` and `starmap`" + return self.zip(cycled=cycled).starmap(f, *args, **kwargs) + +# %% ../nbs/02_foundation.ipynb +@patch +def zipwith(self:L, *rest, cycled=False): + "Create new `L` with `self` zip with each of `*rest`" + return self._new([self, *rest]).zip(cycled=cycled) + +# %% ../nbs/02_foundation.ipynb +@patch +def map_zipwith(self:L, f, *rest, cycled=False, **kwargs): + "Combine `zipwith` and `starmap`" + return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs) + +# %% ../nbs/02_foundation.ipynb +@patch +@curryable def filter(self:L, f=noop, negate=False, **kwargs): "Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`" return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def starfilter(self:L, f, negate=False, **kwargs): "Like `filter`, but unpacks elements as args to `f`" _f = lambda x: f(*x, **kwargs) @@ -212,26 +303,23 @@ def starfilter(self:L, f, negate=False, **kwargs): # %% ../nbs/02_foundation.ipynb @patch +@curryable def rstarfilter(self:L, f, negate=False, **kwargs): "Like `starfilter`, but reverse the order of args" _f = lambda x: f(*x[::-1], **kwargs) if negate: _f = not_(_f) return self._new(filter(_f, self)) -# %% ../nbs/02_foundation.ipynb -@patch(cls_method=True) -def range(cls:L, a, b=None, step=None): - "Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`" - return cls(range_of(a, b=b, step=step)) - # %% ../nbs/02_foundation.ipynb @patch +@curryable def argwhere(self:L, f, negate=False, **kwargs): "Like `filter`, but return indices for matching items" return self._new(argwhere(self, f, negate, **kwargs)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def starargwhere(self:L, f, negate=False): "Like `argwhere`, but unpacks elements as args to `f`" _f = lambda x: f(*x) @@ -240,6 +328,7 @@ def starargwhere(self:L, f, negate=False): # %% ../nbs/02_foundation.ipynb @patch +@curryable def rstarargwhere(self:L, f, negate=False): "Like `starargwhere`, but reverse the order of args" _f = lambda x: f(*x[::-1]) @@ -248,18 +337,7 @@ def rstarargwhere(self:L, f, negate=False): # %% ../nbs/02_foundation.ipynb @patch -def enumerate(self:L): - "Same as `enumerate`" - return L(enumerate(self)) - -# %% ../nbs/02_foundation.ipynb -@patch -def renumerate(self:L): - "Same as `renumerate`" - return L(renumerate(self)) - -# %% ../nbs/02_foundation.ipynb -@patch +@curryable def argfirst(self:L, f, negate=False): "Return index of first matching item" if negate: f = not_(f) @@ -267,6 +345,7 @@ def argfirst(self:L, f, negate=False): # %% ../nbs/02_foundation.ipynb @patch +@curryable def starargfirst(self:L, f, negate=False): "Like `argfirst`, but unpacks elements as args to `f`" _f = lambda x: f(*x) @@ -275,60 +354,13 @@ def starargfirst(self:L, f, negate=False): # %% ../nbs/02_foundation.ipynb @patch +@curryable def rstarargfirst(self:L, f, negate=False): "Like `starargfirst`, but reverse the order of args" _f = lambda x: f(*x[::-1]) if negate: _f = not_(_f) return first(i for i,o in self.enumerate() if _f(o)) -# %% ../nbs/02_foundation.ipynb -@patch -def map(self:L, f, *args, **kwargs): - "Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`" - return self._new(map_ex(self, f, *args, gen=False, **kwargs)) - -# %% ../nbs/02_foundation.ipynb -@patch -def starmap(self:L, f, *args, **kwargs): - "Like `map`, but use `itertools.starmap`" - return self._new(itertools.starmap(partial(f,*args,**kwargs), self)) - -# %% ../nbs/02_foundation.ipynb -@patch -def rstarmap(self:L, f, *args, **kwargs): - "Like `starmap`, but reverse the order of args" - return self._new(itertools.starmap(lambda *x: f(*x[::-1], *args, **kwargs), self)) - -# %% ../nbs/02_foundation.ipynb -@patch -def map_dict(self:L, f=noop, *args, **kwargs): - "Like `map`, but creates a dict from `items` to function results" - return {k:f(k, *args,**kwargs) for k in self} - -# %% ../nbs/02_foundation.ipynb -@patch -def zip(self:L, cycled=False): - "Create new `L` with `zip(*items)`" - return self._new((zip_cycle if cycled else zip)(*self)) - -# %% ../nbs/02_foundation.ipynb -@patch -def map_zip(self:L, f, *args, cycled=False, **kwargs): - "Combine `zip` and `starmap`" - return self.zip(cycled=cycled).starmap(f, *args, **kwargs) - -# %% ../nbs/02_foundation.ipynb -@patch -def zipwith(self:L, *rest, cycled=False): - "Create new `L` with `self` zip with each of `*rest`" - return self._new([self, *rest]).zip(cycled=cycled) - -# %% ../nbs/02_foundation.ipynb -@patch -def map_zipwith(self:L, f, *rest, cycled=False, **kwargs): - "Combine `zipwith` and `starmap`" - return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs) - # %% ../nbs/02_foundation.ipynb @patch def itemgot(self:L, *idxs): @@ -345,18 +377,21 @@ def attrgot(self:L, k, default=None): # %% ../nbs/02_foundation.ipynb @patch +@curryable def sorted(self:L, key=None, reverse=False, cmp=None, **kwargs): "New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`" return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def starsorted(self:L, key, reverse=False): "Like `sorted`, but unpacks elements as args to `key`" return self._new(sorted(self, key=lambda x: key(*x), reverse=reverse)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def rstarsorted(self:L, key, reverse=False): "Like `starsorted`, but reverse the order of args" return self._new(sorted(self, key=lambda x: key(*x[::-1]), reverse=reverse)) @@ -383,12 +418,14 @@ def shuffle(self:L): # %% ../nbs/02_foundation.ipynb @patch +@curryable def reduce(self:L, f, initial=None): "Wrapper for `functools.reduce`" return reduce(f, self) if initial is None else reduce(f, self, initial) # %% ../nbs/02_foundation.ipynb @patch +@curryable def starreduce(self:L, f, initial=None): "Like `reduce`, but unpacks elements as args to `f`" _f = lambda acc, x: f(acc, *x) @@ -396,6 +433,7 @@ def starreduce(self:L, f, initial=None): # %% ../nbs/02_foundation.ipynb @patch +@curryable def rstarreduce(self:L, f, initial=None): "Like `starreduce`, but reverse the order of unpacked args" _f = lambda acc, x: f(acc, *x[::-1]) @@ -433,42 +471,49 @@ def cycle(self:L): # %% ../nbs/02_foundation.ipynb @patch +@curryable def takewhile(self:L, f): "Same as `itertools.takewhile`" return self._new(itertools.takewhile(f, self)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def dropwhile(self:L, f): "Same as `itertools.dropwhile`" return self._new(itertools.dropwhile(f, self)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def startakewhile(self:L, f): "Like `takewhile`, but unpacks elements as args to `f`" return self._new(itertools.takewhile(lambda x: f(*x), self)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def rstartakewhile(self:L, f): "Like `startakewhile`, but reverse the order of args" return self._new(itertools.takewhile(lambda x: f(*x[::-1]), self)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def stardropwhile(self:L, f): "Like `dropwhile`, but unpacks elements as args to `f`" return self._new(itertools.dropwhile(lambda x: f(*x), self)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def rstardropwhile(self:L, f): "Like `stardropwhile`, but reverse the order of args" return self._new(itertools.dropwhile(lambda x: f(*x[::-1]), self)) # %% ../nbs/02_foundation.ipynb @patch +@curryable def accumulate(self:L, f=operator.add, initial=None): "Same as `itertools.accumulate`" return self._new(itertools.accumulate(self, f, initial=initial)) @@ -494,7 +539,7 @@ def _batched(iterable, n): # %% ../nbs/02_foundation.ipynb @patch def batched(self:L, n): - "Same as `itertools.batched`" + "Same as `itertools.batched` (but also works on older Python versions" return self._new(batched(self, n)) # %% ../nbs/02_foundation.ipynb @@ -517,15 +562,16 @@ def combinations(self:L, r): # %% ../nbs/02_foundation.ipynb @patch +@curryable def partition(self:L, f=noop, **kwargs): "Split into two `L`s based on predicate `f`: (true_items, false_items)" a,b = [],[] for o in self: (a if f(o, **kwargs) else b).append(o) return self._new(a),self._new(b) - # %% ../nbs/02_foundation.ipynb @patch +@curryable def starpartition(self:L, f, **kwargs): "Like `partition`, but unpacks elements as args to `f`" a,b = [],[] @@ -534,6 +580,7 @@ def starpartition(self:L, f, **kwargs): # %% ../nbs/02_foundation.ipynb @patch +@curryable def rstarpartition(self:L, f, **kwargs): "Like `starpartition`, but reverse the order of args" a,b = [],[] diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 18c626a9..831f6c9f 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -4,7 +4,9 @@ "cell_type": "code", "execution_count": null, "id": "50bae063", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:43.240601+00:00" + }, "outputs": [], "source": [ "#| default_exp foundation" @@ -14,13 +16,15 @@ "cell_type": "code", "execution_count": null, "id": "0f974791", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:43.242546+00:00" + }, "outputs": [], "source": [ "#| export\n", "from fastcore.imports import *\n", "from fastcore.basics import *\n", - "from functools import lru_cache\n", + "from functools import lru_cache,wraps\n", "from contextlib import contextmanager\n", "from copy import copy\n", "from configparser import ConfigParser\n", @@ -31,13 +35,16 @@ "cell_type": "code", "execution_count": null, "id": "8990087b", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:43.244467+00:00" + }, "outputs": [], "source": [ "#| hide\n", "from fastcore.test import *\n", "from nbdev.showdoc import *\n", - "from fastcore.nb_imports import *" + "from fastcore.nb_imports import *\n", + "import numpy as np" ] }, { @@ -53,7 +60,9 @@ { "cell_type": "markdown", "id": "13ac17ff", - "metadata": {}, + "metadata": { + "heading_collapsed": true + }, "source": [ "## Foundational Functions" ] @@ -62,7 +71,10 @@ "cell_type": "code", "execution_count": null, "id": "f059df0f", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:43.295505+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -79,7 +91,10 @@ "cell_type": "code", "execution_count": null, "id": "c3fd027f", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:43.297043+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -100,7 +115,9 @@ { "cell_type": "markdown", "id": "620dc00e", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "`add_docs` allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in [our style guide](https://docs.fast.ai/dev/style.html).\n", "\n", @@ -111,7 +128,10 @@ "cell_type": "code", "execution_count": null, "id": "0832c567", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:43.378519+00:00" + }, "outputs": [], "source": [ "class T:\n", @@ -122,7 +142,9 @@ { "cell_type": "markdown", "id": "adedd47e", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "You can add documentation to this class like so:" ] @@ -131,7 +153,10 @@ "cell_type": "code", "execution_count": null, "id": "da9d95f9", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:43.789054+00:00" + }, "outputs": [], "source": [ "add_docs(T, cls_doc=\"A docstring for the class.\",\n", @@ -142,7 +167,9 @@ { "cell_type": "markdown", "id": "a91ee046", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "Now, docstrings will appear as expected:" ] @@ -151,7 +178,10 @@ "cell_type": "code", "execution_count": null, "id": "2c4a26aa", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:44.103306+00:00" + }, "outputs": [], "source": [ "test_eq(T.__doc__, \"A docstring for the class.\")\n", @@ -162,7 +192,9 @@ { "cell_type": "markdown", "id": "47545904", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "`add_docs` also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:" ] @@ -171,7 +203,10 @@ "cell_type": "code", "execution_count": null, "id": "a9f77cf6", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:44.194964+00:00" + }, "outputs": [], "source": [ "class T:\n", @@ -186,7 +221,10 @@ "cell_type": "code", "execution_count": null, "id": "88805b49", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:44.276162+00:00" + }, "outputs": [], "source": [ "#| hide\n", @@ -205,7 +243,10 @@ "cell_type": "code", "execution_count": null, "id": "b180313a", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:44.313472+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -218,7 +259,9 @@ { "cell_type": "markdown", "id": "e25bd40f", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "Instead of using `add_docs`, you can use the decorator `docs` as shown below. Note that the docstring for the class can be set with the argument `cls_doc`:" ] @@ -227,7 +270,10 @@ "cell_type": "code", "execution_count": null, "id": "d10f59f9", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:44.417345+00:00" + }, "outputs": [], "source": [ "@docs\n", @@ -248,7 +294,9 @@ { "cell_type": "markdown", "id": "2ad622b1", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "For either the `docs` decorator or the `add_docs` function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the `_docs` attribute:" ] @@ -257,7 +305,10 @@ "cell_type": "code", "execution_count": null, "id": "e4bbd1b9", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:44.539508+00:00" + }, "outputs": [], "source": [ "@docs\n", @@ -275,7 +326,10 @@ "cell_type": "code", "execution_count": null, "id": "0b1f9829", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:44.543215+00:00" + }, "outputs": [ { "data": { @@ -294,7 +348,7 @@ "*Test whether `o` can be used in a `for` loop*" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -307,7 +361,10 @@ "cell_type": "code", "execution_count": null, "id": "4e191b5b", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.130433+00:00" + }, "outputs": [], "source": [ "assert is_iter([1])\n", @@ -320,11 +377,14 @@ "cell_type": "code", "execution_count": null, "id": "34a29b43", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.134924+00:00" + }, "outputs": [], "source": [ "#| export\n", - "def coll_repr(c, max_n=20):\n", + "def coll_repr(c, max_n=250):\n", " \"String repr of up to `max_n` items of (possibly lazy) collection `c`\"\n", " return f'(#{len(c)}) [' + ','.join(itertools.islice(map(repr,c), max_n)) + (\n", " '...' if len(c)>max_n else '') + ']'" @@ -333,7 +393,9 @@ { "cell_type": "markdown", "id": "198216f8", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "`coll_repr` is used to provide a more informative [`__repr__`](https://stackoverflow.com/questions/1984162/purpose-of-pythons-repr) about list-like objects. `coll_repr` and is used by `L` to build a `__repr__` that displays the length of a list in addition to a preview of a list.\n", "\n", @@ -344,7 +406,10 @@ "cell_type": "code", "execution_count": null, "id": "fe3ed954", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.192522+00:00" + }, "outputs": [], "source": [ "test_eq(coll_repr(range(1000),10), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')\n", @@ -357,7 +422,10 @@ "cell_type": "code", "execution_count": null, "id": "6cf96f49", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.195740+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -370,7 +438,10 @@ "cell_type": "code", "execution_count": null, "id": "66a28a53", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.198886+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -389,7 +460,10 @@ "cell_type": "code", "execution_count": null, "id": "3b7dd826", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.234542+00:00" + }, "outputs": [], "source": [ "test_eq(mask2idxs([False,True,False,True]), [1,3])\n", @@ -401,7 +475,10 @@ "cell_type": "code", "execution_count": null, "id": "af754019", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.237172+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -415,7 +492,10 @@ "cell_type": "code", "execution_count": null, "id": "7f8805d7", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.239023+00:00" + }, "outputs": [], "source": [ "test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])\n", @@ -428,7 +508,10 @@ "cell_type": "code", "execution_count": null, "id": "9346b91a", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.240645+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -441,7 +524,10 @@ "cell_type": "code", "execution_count": null, "id": "21c554a1", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.242243+00:00" + }, "outputs": [], "source": [ "test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])" @@ -451,7 +537,10 @@ "cell_type": "code", "execution_count": null, "id": "8a281173", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.276669+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -463,7 +552,9 @@ { "cell_type": "markdown", "id": "2c4497d9", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "You can, for example index a single item in a list with an integer or a 0-dimensional numpy array:" ] @@ -472,7 +563,10 @@ "cell_type": "code", "execution_count": null, "id": "cf94cb54", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.278649+00:00" + }, "outputs": [], "source": [ "assert is_indexer(1)\n", @@ -482,7 +576,9 @@ { "cell_type": "markdown", "id": "504d89e8", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "However, you cannot index into single item in a list with another list or a numpy array with ndim > 0. " ] @@ -491,7 +587,10 @@ "cell_type": "code", "execution_count": null, "id": "1159335e", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.280124+00:00" + }, "outputs": [], "source": [ "assert not is_indexer([1, 2])\n", @@ -502,7 +601,10 @@ "cell_type": "code", "execution_count": null, "id": "3fd3133f", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.281676+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -515,7 +617,10 @@ "cell_type": "code", "execution_count": null, "id": "73a52d33", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.283204+00:00" + }, "outputs": [ { "data": { @@ -523,7 +628,7 @@ "60" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -536,7 +641,10 @@ "cell_type": "code", "execution_count": null, "id": "fa5990de", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.285240+00:00" + }, "outputs": [ { "data": { @@ -544,7 +652,7 @@ "1" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -557,7 +665,10 @@ "cell_type": "code", "execution_count": null, "id": "6f9ef4e6", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.332834+00:00" + }, "outputs": [ { "data": { @@ -565,7 +676,7 @@ "0" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -577,7 +688,9 @@ { "cell_type": "markdown", "id": "d8a026cf", - "metadata": {}, + "metadata": { + "heading_collapsed": true + }, "source": [ "## `L` helpers" ] @@ -586,7 +699,10 @@ "cell_type": "code", "execution_count": null, "id": "eefd2763", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.334989+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -604,7 +720,9 @@ { "cell_type": "markdown", "id": "0ec47e38", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "`ColBase` is a base class that emulates the functionality of a python `list`:" ] @@ -613,7 +731,10 @@ "cell_type": "code", "execution_count": null, "id": "896956c4", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.336439+00:00" + }, "outputs": [], "source": [ "class _T(CollBase): pass\n", @@ -638,7 +759,9 @@ "cell_type": "code", "execution_count": null, "id": "d953eaf9", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:46.337965+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -652,7 +775,9 @@ "cell_type": "code", "execution_count": null, "id": "f571f489", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:46.388886+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -713,7 +838,9 @@ "cell_type": "code", "execution_count": null, "id": "05e35e65", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:46.390290+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -728,8 +855,21 @@ "cell_type": "code", "execution_count": null, "id": "68e52115", - "metadata": {}, - "outputs": [], + "metadata": { + "time_run": "2025-12-20T03:49:46.392040+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "__main__.L" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "#| export\n", "Sequence.register(L);" @@ -743,11 +883,24 @@ "`L` is a drop in replacement for a python `list`. Inspired by [NumPy](http://www.numpy.org/), `L`, supports advanced indexing and has additional methods (outlined below) that provide additional functionality and encourage simple expressive code." ] }, + { + "cell_type": "markdown", + "id": "f523de6f", + "metadata": { + "heading_collapsed": true + }, + "source": [ + "### Examples and overview" + ] + }, { "cell_type": "code", "execution_count": null, "id": "a8a78e40", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.393916+00:00" + }, "outputs": [], "source": [ "from fastcore.utils import gt" @@ -756,7 +909,9 @@ { "cell_type": "markdown", "id": "40393b8d", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "Read [this overview section](https://fastcore.fast.ai/tour.html#L) for a quick tutorial of `L`, as well as background on the name. \n", "\n", @@ -767,7 +922,10 @@ "cell_type": "code", "execution_count": null, "id": "72125daf", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.395404+00:00" + }, "outputs": [ { "data": { @@ -775,7 +933,7 @@ "(#12) [0,1,2,'j',4,'k',6,7,8,9,10,11]" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -796,7 +954,9 @@ { "cell_type": "markdown", "id": "694445fa", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "Any `L` is a `Sequence` so you can use it with methods like `random.sample`:" ] @@ -805,7 +965,10 @@ "cell_type": "code", "execution_count": null, "id": "03129273", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:46.452847+00:00" + }, "outputs": [], "source": [ "assert isinstance(t, Sequence)" @@ -815,7 +978,10 @@ "cell_type": "code", "execution_count": null, "id": "26bd9f4d", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.007291+00:00" + }, "outputs": [], "source": [ "import random" @@ -825,7 +991,10 @@ "cell_type": "code", "execution_count": null, "id": "6786d495", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.189117+00:00" + }, "outputs": [ { "data": { @@ -833,7 +1002,7 @@ "[6, 11, 0]" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -847,7 +1016,10 @@ "cell_type": "code", "execution_count": null, "id": "3212eb40", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.301175+00:00" + }, "outputs": [], "source": [ "#| hide\n", @@ -864,7 +1036,9 @@ { "cell_type": "markdown", "id": "7a91eab9", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "There are optimized indexers for arrays, tensors, and DataFrames." ] @@ -873,7 +1047,10 @@ "cell_type": "code", "execution_count": null, "id": "7de0e777", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.375193+00:00" + }, "outputs": [], "source": [ "import pandas as pd" @@ -883,7 +1060,10 @@ "cell_type": "code", "execution_count": null, "id": "2f32ddb9", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.381016+00:00" + }, "outputs": [], "source": [ "arr = np.arange(9).reshape(3,3)\n", @@ -898,7 +1078,9 @@ { "cell_type": "markdown", "id": "4bf55f8a", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "You can also modify an `L` with `append`, `+`, and `*`." ] @@ -907,7 +1089,10 @@ "cell_type": "code", "execution_count": null, "id": "8fc4ca2e", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.484732+00:00" + }, "outputs": [], "source": [ "t = L()\n", @@ -929,7 +1114,9 @@ { "cell_type": "markdown", "id": "1c498ee5", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "An `L` can be constructed from anything iterable, although tensors and arrays will not be iterated over on construction, unless you pass `use_list` to the constructor." ] @@ -938,7 +1125,10 @@ "cell_type": "code", "execution_count": null, "id": "c9d8144c", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.587852+00:00" + }, "outputs": [], "source": [ "test_eq(L([1,2,3]),[1,2,3])\n", @@ -956,7 +1146,9 @@ { "cell_type": "markdown", "id": "6b378062", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "If `match` is not `None` then the created list is same len as `match`, either by:\n", "\n", @@ -968,7 +1160,10 @@ "cell_type": "code", "execution_count": null, "id": "75d2daf4", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.636301+00:00" + }, "outputs": [], "source": [ "test_eq(L(1,match=[1,2,3]),[1,1,1])\n", @@ -979,7 +1174,9 @@ { "cell_type": "markdown", "id": "c8153cf0", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "If you create an `L` from an existing `L` then you'll get back the original object (since `L` uses the `NewChkMeta` metaclass)." ] @@ -988,7 +1185,10 @@ "cell_type": "code", "execution_count": null, "id": "706d5ac7", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.641477+00:00" + }, "outputs": [], "source": [ "test_is(L(t), t)" @@ -997,7 +1197,9 @@ { "cell_type": "markdown", "id": "89d2c804", - "metadata": {}, + "metadata": { + "hidden": 1 + }, "source": [ "An `L` is considred equal to a list if they have the same elements. It's never considered equal to a `str` a `set` or a `dict` even if they have the same elements/keys." ] @@ -1006,7 +1208,10 @@ "cell_type": "code", "execution_count": null, "id": "a208d40c", - "metadata": {}, + "metadata": { + "hidden": 1, + "time_run": "2025-12-20T03:49:47.721016+00:00" + }, "outputs": [], "source": [ "test_eq(L(['a', 'b']), ['a', 'b'])\n", @@ -1026,7 +1231,9 @@ "cell_type": "code", "execution_count": null, "id": "8ba7bfe3", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:47.762838+00:00" + }, "outputs": [ { "data": { @@ -1047,7 +1254,7 @@ "*Retrieve `idx` (can be list of indices, or mask, or int) items*" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -1060,7 +1267,9 @@ "cell_type": "code", "execution_count": null, "id": "a6edf562", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.042534+00:00" + }, "outputs": [], "source": [ "t = L(range(12))\n", @@ -1075,7 +1284,9 @@ "cell_type": "code", "execution_count": null, "id": "93ffea3b", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.091917+00:00" + }, "outputs": [ { "data": { @@ -1096,7 +1307,7 @@ "*Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable)*" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -1109,7 +1320,9 @@ "cell_type": "code", "execution_count": null, "id": "b84c56e3", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.134862+00:00" + }, "outputs": [], "source": [ "t[4,6] = 0\n", @@ -1122,7 +1335,9 @@ "cell_type": "code", "execution_count": null, "id": "d966b525", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.141605+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -1136,7 +1351,9 @@ "cell_type": "code", "execution_count": null, "id": "62aa9a1a", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.148221+00:00" + }, "outputs": [], "source": [ "test_eq(L(4,1,2,3,4,4).unique(), [4,1,2,3])" @@ -1146,7 +1363,9 @@ "cell_type": "code", "execution_count": null, "id": "840aef37", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.154657+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -1160,17 +1379,106 @@ "cell_type": "code", "execution_count": null, "id": "76dd07e3", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.161156+00:00" + }, "outputs": [], "source": [ "test_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1})" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2e038cc", + "metadata": { + "time_run": "2025-12-20T03:49:48.167659+00:00" + }, + "outputs": [], + "source": [ + "#| export\n", + "@patch(cls_method=True)\n", + "def range(cls:L, a, b=None, step=None):\n", + " \"Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`\"\n", + " return cls(range_of(a, b=b, step=step))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7d0109a", + "metadata": { + "time_run": "2025-12-20T03:49:48.172271+00:00" + }, + "outputs": [], + "source": [ + "test_eq_type(L.range([1,1,1]), L(range(3)))\n", + "test_eq_type(L.range(5,2,2), L(range(5,2,2)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1d16238", + "metadata": { + "time_run": "2025-12-20T04:00:47.965252+00:00" + }, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def enumerate(self:L):\n", + " \"Same as `enumerate`\"\n", + " return L(enumerate(self))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4592473d", + "metadata": { + "time_run": "2025-12-20T04:00:48.374391+00:00" + }, + "outputs": [], + "source": [ + "test_eq(L('a','b','c').enumerate(), [(0,'a'),(1,'b'),(2,'c')])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccd5c96c", + "metadata": { + "time_run": "2025-12-20T04:00:48.891136+00:00" + }, + "outputs": [], + "source": [ + "#| export\n", + "@patch\n", + "def renumerate(self:L):\n", + " \"Same as `renumerate`\"\n", + " return L(renumerate(self))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3fd03f76", + "metadata": { + "time_run": "2025-12-20T04:00:49.403500+00:00" + }, + "outputs": [], + "source": [ + "test_eq(L('a','b','c').renumerate(), [('a', 0), ('b', 1), ('c', 2)])" + ] + }, { "cell_type": "code", "execution_count": null, "id": "13795373", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.181438+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -1192,7 +1500,9 @@ "cell_type": "code", "execution_count": null, "id": "5b9583f3", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.188171+00:00" + }, "outputs": [], "source": [ "test_eq(L.split('a b c'), ['a','b','c'])\n", @@ -1204,7 +1514,9 @@ "cell_type": "code", "execution_count": null, "id": "6da7b43c", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.195107+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -1226,7 +1538,9 @@ "cell_type": "code", "execution_count": null, "id": "2508910d", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T03:49:48.203068+00:00" + }, "outputs": [], "source": [ "test_eq(L.splitlines('a\\nb\\nc'), ['a','b','c'])\n", @@ -1236,694 +1550,1116 @@ { "cell_type": "code", "execution_count": null, - "id": "148cc15c", - "metadata": {}, + "id": "826ffa88", + "metadata": { + "time_run": "2025-12-20T03:49:48.210412+00:00" + }, "outputs": [], "source": [ "#| export\n", - "@patch\n", - "def groupby(self:L, key, val=noop):\n", - " \"Same as `fastcore.basics.groupby`\"\n", - " return groupby(self, key, val=val)" + "def curryable(f):\n", + " @wraps(f)\n", + " def wrapper(self, *args, **kwargs):\n", + " if not isinstance(self, L): return lambda items: f(L(items), self, *args, **kwargs)\n", + " return f(L(self), *args, **kwargs)\n", + " return wrapper" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "4c88e24d", + "cell_type": "markdown", + "id": "e773b244", "metadata": {}, - "outputs": [], "source": [ - "words = L.split('aaa abc bba')\n", - "test_eq(words.groupby(0, (1,2)), {'a':[('a','a'),('b','c')], 'b':[('b','a')]})" + "The `curryable` decorator enables a powerful pattern: methods decorated with it can be called either as instance methods (the normal way) or as class methods that return a partial function.\n", + "\n", + "For instance, consider processing nested data structures. Without curryable, you'd write:\n", + "\n", + "```python\n", + "L(lines).map(lambda x: L(x).map(int))\n", + "```\n", + "\n", + "With curryable, you can write:\n", + "\n", + "```python\n", + "L(lines).map(L.map(int))\n", + "```\n", + "\n", + "When you call `L.map(int)` on the class (not an instance), the decorator returns a `functools.partial` that waits for an iterable to be passed in later.\n", + "\n", + "This pattern is especially valuable for data parsing pipelines where you're frequently mapping transformations over nested structures. The curried form reads more naturally and composes well with other curried functions like `splitter()` and `linesplitter()`." ] }, { "cell_type": "code", "execution_count": null, - "id": "66e76d03", - "metadata": {}, + "id": "0d6407c6", + "metadata": { + "time_run": "2025-12-20T03:49:48.217512+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def filter(self:L, f=noop, negate=False, **kwargs):\n", - " \"Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`\"\n", - " return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs))" + "@curryable\n", + "def map(self:L, f, *args, **kwargs):\n", + " \"Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`\"\n", + " return self._new(map_ex(self, f, *args, gen=False, **kwargs))" ] }, { "cell_type": "code", "execution_count": null, - "id": "79f76b77", + "id": "566326ab", + "metadata": { + "time_run": "2025-12-20T03:49:48.224338+00:00" + }, + "outputs": [], + "source": [ + "test_eq(L.range(4).map(operator.neg), [0,-1,-2,-3])" + ] + }, + { + "cell_type": "markdown", + "id": "e914979e", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[0, 1, 2, 3, 1, 5, 2, 7, 8, 9, 10, 11]" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "list(t)" + "If `f` is a string then it is treated as a format string to create the mapping:" ] }, { "cell_type": "code", "execution_count": null, - "id": "99fc6729", - "metadata": {}, + "id": "24680aae", + "metadata": { + "time_run": "2025-12-20T03:49:48.231016+00:00" + }, "outputs": [], "source": [ - "test_eq(t.filter(lambda o:o<5), [0,1,2,3,1,2])\n", - "test_eq(t.filter(lambda o:o<5, negate=True), [5,7,8,9,10,11])" + "test_eq(L.range(4).map('#{}#'), ['#0#','#1#','#2#','#3#'])" + ] + }, + { + "cell_type": "markdown", + "id": "60d32d03", + "metadata": {}, + "source": [ + "If `f` is a dictionary (or anything supporting `__getitem__`) then it is indexed to create the mapping:" ] }, { "cell_type": "code", "execution_count": null, - "id": "085b690b", - "metadata": {}, + "id": "57657757", + "metadata": { + "time_run": "2025-12-20T03:49:48.236175+00:00" + }, "outputs": [], "source": [ - "#| export\n", - "@patch\n", - "def starfilter(self:L, f, negate=False, **kwargs):\n", - " \"Like `filter`, but unpacks elements as args to `f`\"\n", - " _f = lambda x: f(*x, **kwargs)\n", - " if negate: _f = not_(_f)\n", - " return self._new(filter(_f, self))" + "test_eq(L.range(4).map(list('abcd')), list('abcd'))" ] }, { "cell_type": "markdown", - "id": "b4a4837f", + "id": "659406f2", "metadata": {}, "source": [ - "`L.starfilter` is like `filter`, but unpacks tuple elements as arguments to the predicate:" + "You can also pass the same `arg` params that `bind` accepts:" ] }, { "cell_type": "code", "execution_count": null, - "id": "cb878281", - "metadata": {}, + "id": "4d14fb0f", + "metadata": { + "time_run": "2025-12-20T03:49:48.244732+00:00" + }, "outputs": [], "source": [ - "test_eq(L((1,2),(3,1),(2,3)).starfilter(lt), [(1,2),(2,3)])\n", - "test_eq(L((1,2),(3,1),(2,3)).starfilter(lt, negate=True), [(3,1)])" + "def f(a=None,b=None): return b\n", + "test_eq(L.range(4).map(f, b=arg0), range(4))" ] }, { "cell_type": "code", "execution_count": null, - "id": "e0fa80df", - "metadata": {}, + "id": "9c5a4633", + "metadata": { + "time_run": "2025-12-20T03:49:48.251205+00:00" + }, "outputs": [], "source": [ "#| export\n", - "@patch\n", - "def rstarfilter(self:L, f, negate=False, **kwargs):\n", - " \"Like `starfilter`, but reverse the order of args\"\n", - " _f = lambda x: f(*x[::-1], **kwargs)\n", - " if negate: _f = not_(_f)\n", - " return self._new(filter(_f, self))" + "def splitter(sep=None, maxsplit=-1):\n", + " \"Create a partial function that splits strings into `L`\"\n", + " return partial(L.split, sep=sep, maxsplit=maxsplit)" ] }, { "cell_type": "markdown", - "id": "6392c73c", + "id": "46a6fbb8", "metadata": {}, "source": [ - "`L.rstarfilter` is like `starfilter`, but reverses the order of unpacked arguments:" + "A curried version of `L.split`, useful for mapping over collections of strings. For instance to split some lines with the same separator:" ] }, { "cell_type": "code", "execution_count": null, - "id": "a26c1ac4", - "metadata": {}, - "outputs": [], - "source": [ - "test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt), [(2,1),(3,2)]) # 1<2, 3<1 fails, 2<3\n", - "test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt, negate=True), [(1,3)])" - ] - }, + "id": "047fc5bf", + "metadata": { + "time_run": "2025-12-20T03:49:48.257555+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#3) [['1', '2', '3'],['4', '5', '6'],['7', '8', '9']]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data = '''1,2,3\n", + "4,5,6\n", + "7,8,9'''\n", + "\n", + "grid = L.splitlines(data).map(splitter(','))\n", + "grid" + ] + }, + { + "cell_type": "markdown", + "id": "4cb739bc", + "metadata": { + "use_thinking": true + }, + "source": [ + "As mentioned in the `curryable` discussion, `map` can be curried. This can work well together with `L.splitlines` output:" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "4b4a7bc4", + "id": "d3f55753", + "metadata": { + "time_run": "2025-12-20T03:49:48.263191+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#3) [[1, 2, 3],[4, 5, 6],[7, 8, 9]]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "intgrid = grid.map(L.map(int))\n", + "intgrid" + ] + }, + { + "cell_type": "markdown", + "id": "c11989d1", "metadata": {}, - "outputs": [], "source": [ - "#| export\n", - "@patch(cls_method=True)\n", - "def range(cls:L, a, b=None, step=None):\n", - " \"Class Method: Same as `range`, but returns `L`. Can pass collection for `a`, to use `len(a)`\"\n", - " return cls(range_of(a, b=b, step=step))" + "Although in this particular example numpy has a useful shortcut:" ] }, { "cell_type": "code", "execution_count": null, - "id": "42a5923c", - "metadata": {}, - "outputs": [], + "id": "6665dc80", + "metadata": { + "time_run": "2025-12-20T03:50:09.736968+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1, 2, 3],\n", + " [4, 5, 6],\n", + " [7, 8, 9]])" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "test_eq_type(L.range([1,1,1]), L(range(3)))\n", - "test_eq_type(L.range(5,2,2), L(range(5,2,2)))" + "np.genfromtxt(data.splitlines(), delimiter=',', dtype=int)" ] }, { "cell_type": "code", "execution_count": null, - "id": "a2515ab8", - "metadata": {}, + "id": "ce5b413b", + "metadata": { + "time_run": "2025-12-20T03:50:12.040162+00:00" + }, "outputs": [], "source": [ "#| export\n", - "@patch\n", - "def argwhere(self:L, f, negate=False, **kwargs):\n", - " \"Like `filter`, but return indices for matching items\"\n", - " return self._new(argwhere(self, f, negate, **kwargs))" + "def linesplitter(keepends=False):\n", + " \"Create a partial function that splits strings by lines into `L`\"\n", + " return partial(L.splitlines, keepends=keepends)" + ] + }, + { + "cell_type": "markdown", + "id": "99021853", + "metadata": {}, + "source": [ + "A curried version of `L.splitlines`, useful for splitting multi-line strings into `L`s when mapping over a collection." ] }, { "cell_type": "code", "execution_count": null, - "id": "af037334", - "metadata": {}, - "outputs": [], + "id": "51b78c23", + "metadata": { + "time_run": "2025-12-20T03:50:13.701721+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#2) [['a', 'b', 'c'],['d', 'e']]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6])" + "L(['a\\nb\\nc', 'd\\ne']).map(linesplitter())" ] }, { "cell_type": "code", "execution_count": null, - "id": "cf22acd4", - "metadata": {}, + "id": "d2680cbd", + "metadata": { + "time_run": "2025-12-20T03:50:15.049900+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def starargwhere(self:L, f, negate=False):\n", - " \"Like `argwhere`, but unpacks elements as args to `f`\"\n", - " _f = lambda x: f(*x)\n", - " if negate: _f = not_(_f)\n", - " return self._new(i for i,o in enumerate(self) if _f(o))" + "@curryable\n", + "def groupby(self:L, key, val=noop):\n", + " \"Same as `fastcore.basics.groupby`\"\n", + " return groupby(self, key, val=val)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0db8cbb", + "metadata": { + "time_run": "2025-12-20T03:50:15.952094+00:00" + }, + "outputs": [], + "source": [ + "words = L.split('aaa abc bba')\n", + "test_eq(words.groupby(0, (1,2)), {'a':[('a','a'),('b','c')], 'b':[('b','a')]})" ] }, { "cell_type": "markdown", - "id": "86913776", + "id": "10c88fca", "metadata": {}, "source": [ - "`L.starargwhere` is like `argwhere`, but unpacks tuple elements as arguments to the predicate:" + "`L.groupby` can also be used in curried form, which is useful when you need to apply the same grouping operation across multiple collections." ] }, { "cell_type": "code", "execution_count": null, - "id": "a4766805", - "metadata": {}, - "outputs": [], + "id": "f7669c31", + "metadata": { + "time_run": "2025-12-20T03:50:17.817264+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#2) [{'a': ['a1', 'a3'], 'b': ['b2']},{'x': ['x1', 'x3'], 'y': ['y2']}]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt), [0,2])\n", - "test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt, negate=True), [1])" + "L([['a1','b2','a3'], ['x1','y2','x3']]).map(L.groupby(0))" ] }, { "cell_type": "code", "execution_count": null, - "id": "1f010f9c", - "metadata": {}, + "id": "37dc972a", + "metadata": { + "time_run": "2025-12-20T03:50:22.681900+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def rstarargwhere(self:L, f, negate=False):\n", - " \"Like `starargwhere`, but reverse the order of args\"\n", - " _f = lambda x: f(*x[::-1])\n", - " if negate: _f = not_(_f)\n", - " return self._new(i for i,o in enumerate(self) if _f(o))" + "@curryable\n", + "def starmap(self:L, f, *args, **kwargs):\n", + " \"Like `map`, but use `itertools.starmap`\"\n", + " return self._new(itertools.starmap(partial(f,*args,**kwargs), self))" ] }, { "cell_type": "markdown", - "id": "049e0c39", + "id": "05c5169f", "metadata": {}, "source": [ - "`L.rstarargwhere` is like `starargwhere`, but reverses the order of unpacked arguments:" + "`L.starmap` applies a function to each element, unpacking tuples as arguments:" ] }, { "cell_type": "code", "execution_count": null, - "id": "155730fd", - "metadata": {}, + "id": "45316c68", + "metadata": { + "time_run": "2025-12-20T03:50:42.002132+00:00" + }, "outputs": [], "source": [ - "test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt), [0,2]) # 1<2, 3<1 fails, 2<3\n", - "test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt, negate=True), [1])" + "test_eq(L([(1,2),(3,4)]).starmap(operator.add), [3,7])\n", + "test_eq(L([(1,2,3),(4,5,6)]).starmap(lambda a,b,c: a+b*c), [7,34])" + ] + }, + { + "cell_type": "markdown", + "id": "d6e01c34", + "metadata": {}, + "source": [ + "The curried form of `L.starmap` is useful when you need to apply the same starmap operation across nested structures. For example, when you have a list of lists of tuples and want to apply a function that unpacks each tuple:" ] }, { "cell_type": "code", "execution_count": null, - "id": "32f6dbce", - "metadata": {}, + "id": "0acf95c8", + "metadata": { + "time_run": "2025-12-20T03:52:05.851645+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#2) [[2, 12],[30, 56]]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested = L([[(1,2),(3,4)], [(5,6),(7,8)]])\n", + "nested.map(L.starmap(operator.mul))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb2283b0", + "metadata": { + "time_run": "2025-12-20T03:52:24.774589+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def enumerate(self:L):\n", - " \"Same as `enumerate`\"\n", - " return L(enumerate(self))" + "@curryable\n", + "def rstarmap(self:L, f, *args, **kwargs):\n", + " \"Like `starmap`, but reverse the order of args\"\n", + " return self._new(itertools.starmap(lambda *x: f(*x[::-1], *args, **kwargs), self))" + ] + }, + { + "cell_type": "markdown", + "id": "f8a15927", + "metadata": {}, + "source": [ + "`L.rstarmap` is like `starmap`, but reverses the order of unpacked arguments:" ] }, { "cell_type": "code", "execution_count": null, - "id": "798d5bf2", - "metadata": {}, + "id": "5c03c922", + "metadata": { + "time_run": "2025-12-20T03:52:27.186565+00:00" + }, "outputs": [], "source": [ - "test_eq(L('a','b','c').enumerate(), [(0,'a'),(1,'b'),(2,'c')])" + "test_eq(L((1,2),(3,4)).rstarmap(operator.sub), [1,1]) # 2-1, 4-3\n", + "test_eq(L(('a','b'),('c','d')).rstarmap('{}{}'.format), ['ba','dc'])" + ] + }, + { + "cell_type": "markdown", + "id": "4a7d40b7", + "metadata": {}, + "source": [ + "The curried form of `L.rstarmap` is useful when mapping over nested structures where you need reversed argument order. This commonly occurs when processing pairs where the second element should be the first argument to a function:" ] }, { "cell_type": "code", "execution_count": null, - "id": "a10fa9d5", - "metadata": {}, + "id": "9c3815ac", + "metadata": { + "time_run": "2025-12-20T03:52:56.445761+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#2) [['1x', '2y'],['3z']]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested = L([[('x',1),('y',2)], [('z',3)]])\n", + "nested.map(L.rstarmap('{}{}'.format))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0846f2e", + "metadata": { + "time_run": "2025-12-20T03:53:02.196257+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def renumerate(self:L):\n", - " \"Same as `renumerate`\"\n", - " return L(renumerate(self))" + "def map_dict(self:L, f=noop, *args, **kwargs):\n", + " \"Like `map`, but creates a dict from `items` to function results\"\n", + " return {k:f(k, *args,**kwargs) for k in self}" ] }, { "cell_type": "code", "execution_count": null, - "id": "e5a49896", - "metadata": {}, + "id": "92f098d8", + "metadata": { + "time_run": "2025-12-20T03:53:03.062053+00:00" + }, "outputs": [], "source": [ - "test_eq(L('a','b','c').renumerate(), [('a', 0), ('b', 1), ('c', 2)])" + "test_eq(L(range(1,5)).map_dict(), {1:1, 2:2, 3:3, 4:4})\n", + "test_eq(L(range(1,5)).map_dict(operator.neg), {1:-1, 2:-2, 3:-3, 4:-4})" ] }, { "cell_type": "code", "execution_count": null, - "id": "3e7fa556", - "metadata": {}, + "id": "105ac114", + "metadata": { + "time_run": "2025-12-20T03:53:56.747629+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def argfirst(self:L, f, negate=False):\n", - " \"Return index of first matching item\"\n", - " if negate: f = not_(f)\n", - " return first(i for i,o in self.enumerate() if f(o))" + "def zip(self:L, cycled=False):\n", + " \"Create new `L` with `zip(*items)`\"\n", + " return self._new((zip_cycle if cycled else zip)(*self))" ] }, { "cell_type": "code", "execution_count": null, - "id": "eee16654", - "metadata": {}, + "id": "b9883e0c", + "metadata": { + "time_run": "2025-12-20T03:53:57.398783+00:00" + }, "outputs": [], "source": [ - "test_eq(t.argfirst(lambda o:o>4), 5)\n", - "test_eq(t.argfirst(lambda o:o>4,negate=True),0)" + "t = L([[1,2,3],'abc'])\n", + "test_eq(t.zip(), [(1, 'a'),(2, 'b'),(3, 'c')])" ] }, { "cell_type": "code", "execution_count": null, - "id": "90509c05", - "metadata": {}, + "id": "702912aa", + "metadata": { + "time_run": "2025-12-20T03:53:58.299915+00:00" + }, "outputs": [], "source": [ - "#| export\n", - "@patch\n", - "def starargfirst(self:L, f, negate=False):\n", - " \"Like `argfirst`, but unpacks elements as args to `f`\"\n", - " _f = lambda x: f(*x)\n", - " if negate: _f = not_(_f)\n", - " return first(i for i,o in self.enumerate() if _f(o))" + "t = L([[1,2,3,4],['a','b','c']])\n", + "test_eq(t.zip(cycled=True ), [(1, 'a'),(2, 'b'),(3, 'c'),(4, 'a')])\n", + "test_eq(t.zip(cycled=False), [(1, 'a'),(2, 'b'),(3, 'c')])" ] }, { - "cell_type": "markdown", - "id": "8ff3d6cc", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "id": "6340b2b3", + "metadata": { + "time_run": "2025-12-20T03:54:59.890978+00:00" + }, + "outputs": [], "source": [ - "`L.starargfirst` is like `argfirst`, but unpacks tuple elements as arguments to the predicate:" + "#| export\n", + "@patch\n", + "def map_zip(self:L, f, *args, cycled=False, **kwargs):\n", + " \"Combine `zip` and `starmap`\"\n", + " return self.zip(cycled=cycled).starmap(f, *args, **kwargs)" ] }, { "cell_type": "code", "execution_count": null, - "id": "d4044d5c", - "metadata": {}, + "id": "d645b000", + "metadata": { + "time_run": "2025-12-20T03:55:00.603864+00:00" + }, "outputs": [], "source": [ - "test_eq(L((3,1),(1,2),(2,3)).starargfirst(lt), 1)\n", - "test_eq(L((1,2),(3,1),(2,3)).starargfirst(lt, negate=True), 1)" + "t = L([1,2,3],[2,3,4])\n", + "test_eq(t.map_zip(operator.mul), [2,6,12])" ] }, { "cell_type": "code", "execution_count": null, - "id": "dc43b9b9", - "metadata": {}, + "id": "bc012fad", + "metadata": { + "time_run": "2025-12-20T03:55:08.964459+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def rstarargfirst(self:L, f, negate=False):\n", - " \"Like `starargfirst`, but reverse the order of args\"\n", - " _f = lambda x: f(*x[::-1])\n", - " if negate: _f = not_(_f)\n", - " return first(i for i,o in self.enumerate() if _f(o))" + "def zipwith(self:L, *rest, cycled=False):\n", + " \"Create new `L` with `self` zip with each of `*rest`\"\n", + " return self._new([self, *rest]).zip(cycled=cycled)" ] }, { - "cell_type": "markdown", - "id": "3aa280a5", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "id": "f10ffe4f", + "metadata": { + "time_run": "2025-12-20T03:55:09.737642+00:00" + }, + "outputs": [], "source": [ - "`L.rstarargfirst` is like `starargfirst`, but reverses the order of unpacked arguments:" + "b = [[0],[1],[2,2]]\n", + "t = L([1,2,3]).zipwith(b)\n", + "test_eq(t, [(1,[0]), (2,[1]), (3,[2,2])])" ] }, { "cell_type": "code", "execution_count": null, - "id": "8dd7abc2", - "metadata": {}, + "id": "5a40226f", + "metadata": { + "time_run": "2025-12-20T03:55:12.144197+00:00" + }, "outputs": [], "source": [ - "test_eq(L((1,3),(2,1),(3,2)).rstarargfirst(lt), 1) # 3<1 fails, 1<2\n", - "test_eq(L((2,1),(1,3),(3,2)).rstarargfirst(lt, negate=True), 1)" + "#| export\n", + "@patch\n", + "def map_zipwith(self:L, f, *rest, cycled=False, **kwargs):\n", + " \"Combine `zipwith` and `starmap`\"\n", + " return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs)" ] }, { "cell_type": "code", "execution_count": null, - "id": "3979b19a", - "metadata": {}, + "id": "bf653b2b", + "metadata": { + "time_run": "2025-12-20T03:55:12.736535+00:00" + }, + "outputs": [], + "source": [ + "test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66e76d03", + "metadata": { + "time_run": "2025-12-20T03:56:48.552651+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def map(self:L, f, *args, **kwargs):\n", - " \"Create new `L` with `f` applied to all `items`, passing `args` and `kwargs` to `f`\"\n", - " return self._new(map_ex(self, f, *args, gen=False, **kwargs))" + "@curryable\n", + "def filter(self:L, f=noop, negate=False, **kwargs):\n", + " \"Create new `L` filtered by predicate `f`, passing `args` and `kwargs` to `f`\"\n", + " return self._new(filter_ex(self, f=f, negate=negate, gen=False, **kwargs))" ] }, { "cell_type": "code", "execution_count": null, - "id": "2e6daf35", - "metadata": {}, + "id": "669d3a5c", + "metadata": { + "time_run": "2025-12-20T03:56:49.207492+00:00" + }, "outputs": [], "source": [ - "test_eq(L.range(4).map(operator.neg), [0,-1,-2,-3])" + "t = L(range(12))\n", + "test_eq(t.filter(lambda o:o<5), [0,1,2,3,4])\n", + "test_eq(t.filter(lambda o:o<5, negate=True), [5,6,7,8,9,10,11])" ] }, { "cell_type": "markdown", - "id": "a1c6950b", + "id": "46c9cce3", "metadata": {}, "source": [ - "If `f` is a string then it is treated as a format string to create the mapping:" + "`L.filter` can be used as a curried class method, returning a partial that filters any iterable and wraps the result in an `L`. This is useful when mapping a filter operation over nested collections." ] }, { "cell_type": "code", "execution_count": null, - "id": "1169e4cb", - "metadata": {}, + "id": "616fed64", + "metadata": { + "time_run": "2025-12-20T03:57:06.416582+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#3) [[1, 2, 3],[4, 5, 6],[7, 8, 9]]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "intgrid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1735243", + "metadata": { + "time_run": "2025-12-20T03:57:01.213394+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#3) [[],[5, 6],[7, 8, 9]]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "intgrid.map(L.filter(ge(5)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "085b690b", + "metadata": { + "time_run": "2025-12-20T03:57:10.630220+00:00" + }, "outputs": [], "source": [ - "test_eq(L.range(4).map('#{}#'), ['#0#','#1#','#2#','#3#'])" + "#| export\n", + "@patch\n", + "@curryable\n", + "def starfilter(self:L, f, negate=False, **kwargs):\n", + " \"Like `filter`, but unpacks elements as args to `f`\"\n", + " _f = lambda x: f(*x, **kwargs)\n", + " if negate: _f = not_(_f)\n", + " return self._new(filter(_f, self))" ] }, { "cell_type": "markdown", - "id": "a27c80f8", + "id": "b4a4837f", "metadata": {}, "source": [ - "If `f` is a dictionary (or anything supporting `__getitem__`) then it is indexed to create the mapping:" + "`L.starfilter` is like `filter`, but unpacks tuple elements as arguments to the predicate:" ] }, { "cell_type": "code", "execution_count": null, - "id": "64dcec5c", - "metadata": {}, + "id": "cb878281", + "metadata": { + "time_run": "2025-12-20T03:57:12.890479+00:00" + }, "outputs": [], "source": [ - "test_eq(L.range(4).map(list('abcd')), list('abcd'))" + "test_eq(L((1,2),(3,1),(2,3)).starfilter(lt), [(1,2),(2,3)])\n", + "test_eq(L((1,2),(3,1),(2,3)).starfilter(lt, negate=True), [(3,1)])" ] }, { "cell_type": "markdown", - "id": "004de9c3", + "id": "3af2fbc1", "metadata": {}, "source": [ - "You can also pass the same `arg` params that `bind` accepts:" + "Curried `L.starfilter` is useful when mapping a starfilter operation over nested collections—each inner collection gets filtered by unpacking its tuples as arguments to the predicate, eg to filter pairs where first < second, across multiple lists of pairs:" ] }, { "cell_type": "code", "execution_count": null, - "id": "ff80bb06", - "metadata": {}, - "outputs": [], + "id": "e939f0d3", + "metadata": { + "time_run": "2025-12-20T03:58:42.289932+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#2) [[(1, 5)],[(4, 6)]]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "def f(a=None,b=None): return b\n", - "test_eq(L.range(4).map(f, b=arg0), range(4))" + "nested = L([[(1,5),(3,2)], [(4,6),(9,1)]])\n", + "nested.map(L.starfilter(lt))" ] }, { "cell_type": "code", "execution_count": null, - "id": "dc2293d3", - "metadata": {}, + "id": "e0fa80df", + "metadata": { + "time_run": "2025-12-20T03:58:48.417077+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def starmap(self:L, f, *args, **kwargs):\n", - " \"Like `map`, but use `itertools.starmap`\"\n", - " return self._new(itertools.starmap(partial(f,*args,**kwargs), self))" + "@curryable\n", + "def rstarfilter(self:L, f, negate=False, **kwargs):\n", + " \"Like `starfilter`, but reverse the order of args\"\n", + " _f = lambda x: f(*x[::-1], **kwargs)\n", + " if negate: _f = not_(_f)\n", + " return self._new(filter(_f, self))" ] }, { "cell_type": "markdown", - "id": "bb8266d4", + "id": "6392c73c", "metadata": {}, "source": [ - "`L.starmap` applies a function to each element, unpacking tuples as arguments:" + "`L.rstarfilter` is like `starfilter`, but reverses the order of unpacked arguments (and can also be curried):" ] }, { "cell_type": "code", "execution_count": null, - "id": "1f8fdde3", - "metadata": {}, + "id": "a26c1ac4", + "metadata": { + "time_run": "2025-12-20T03:59:03.561807+00:00" + }, "outputs": [], "source": [ - "test_eq(L([(1,2),(3,4)]).starmap(operator.add), [3,7])\n", - "test_eq(L([(1,2,3),(4,5,6)]).starmap(lambda a,b,c: a+b*c), [7,34])" + "test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt), [(2,1),(3,2)]) # 1<2, 3<1 fails, 2<3\n", + "test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt, negate=True), [(1,3)])" ] }, { "cell_type": "code", "execution_count": null, - "id": "ddc5b018", - "metadata": {}, + "id": "a2515ab8", + "metadata": { + "time_run": "2025-12-20T03:59:04.220635+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def rstarmap(self:L, f, *args, **kwargs):\n", - " \"Like `starmap`, but reverse the order of args\"\n", - " return self._new(itertools.starmap(lambda *x: f(*x[::-1], *args, **kwargs), self))" - ] - }, - { - "cell_type": "markdown", - "id": "cd0db578", - "metadata": {}, - "source": [ - "`L.rstarmap` is like `starmap`, but reverses the order of unpacked arguments:" + "@curryable\n", + "def argwhere(self:L, f, negate=False, **kwargs):\n", + " \"Like `filter`, but return indices for matching items\"\n", + " return self._new(argwhere(self, f, negate, **kwargs))" ] }, { "cell_type": "code", "execution_count": null, - "id": "fe98f512", - "metadata": {}, + "id": "6f637a43", + "metadata": { + "time_run": "2025-12-20T03:59:53.723656+00:00" + }, "outputs": [], "source": [ - "test_eq(L((1,2),(3,4)).rstarmap(operator.sub), [1,1]) # 2-1, 4-3\n", - "test_eq(L(('a','b'),('c','d')).rstarmap('{}{}'.format), ['ba','dc'])" + "t = L([0,1,2,3,4,99,0])\n", + "test_eq(t.argwhere(lambda o:o<5), [0,1,2,3,4,6])" ] }, { "cell_type": "code", "execution_count": null, - "id": "2356d414", - "metadata": {}, + "id": "cf22acd4", + "metadata": { + "time_run": "2025-12-20T04:00:06.155115+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def map_dict(self:L, f=noop, *args, **kwargs):\n", - " \"Like `map`, but creates a dict from `items` to function results\"\n", - " return {k:f(k, *args,**kwargs) for k in self}" + "@curryable\n", + "def starargwhere(self:L, f, negate=False):\n", + " \"Like `argwhere`, but unpacks elements as args to `f`\"\n", + " _f = lambda x: f(*x)\n", + " if negate: _f = not_(_f)\n", + " return self._new(i for i,o in enumerate(self) if _f(o))" + ] + }, + { + "cell_type": "markdown", + "id": "86913776", + "metadata": {}, + "source": [ + "`L.starargwhere` is like `argwhere`, but unpacks tuple elements as arguments to the predicate (it is also curryable):" ] }, { "cell_type": "code", "execution_count": null, - "id": "9539b84f", - "metadata": {}, + "id": "a4766805", + "metadata": { + "time_run": "2025-12-20T04:00:19.931053+00:00" + }, "outputs": [], "source": [ - "test_eq(L(range(1,5)).map_dict(), {1:1, 2:2, 3:3, 4:4})\n", - "test_eq(L(range(1,5)).map_dict(operator.neg), {1:-1, 2:-2, 3:-3, 4:-4})" + "test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt), [0,2])\n", + "test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt, negate=True), [1])" ] }, { "cell_type": "code", "execution_count": null, - "id": "27c2ec12", - "metadata": {}, + "id": "1f010f9c", + "metadata": { + "time_run": "2025-12-20T04:00:20.396811+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def zip(self:L, cycled=False):\n", - " \"Create new `L` with `zip(*items)`\"\n", - " return self._new((zip_cycle if cycled else zip)(*self))" + "@curryable\n", + "def rstarargwhere(self:L, f, negate=False):\n", + " \"Like `starargwhere`, but reverse the order of args\"\n", + " _f = lambda x: f(*x[::-1])\n", + " if negate: _f = not_(_f)\n", + " return self._new(i for i,o in enumerate(self) if _f(o))" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "87f23d41", + "cell_type": "markdown", + "id": "049e0c39", "metadata": {}, - "outputs": [], "source": [ - "t = L([[1,2,3],'abc'])\n", - "test_eq(t.zip(), [(1, 'a'),(2, 'b'),(3, 'c')])" + "`L.rstarargwhere` is like `starargwhere`, but reverses the order of unpacked arguments (it is also curryable):" ] }, { "cell_type": "code", "execution_count": null, - "id": "6d02ff1c", - "metadata": {}, + "id": "155730fd", + "metadata": { + "time_run": "2025-12-20T04:00:23.542229+00:00" + }, "outputs": [], "source": [ - "t = L([[1,2,3,4],['a','b','c']])\n", - "test_eq(t.zip(cycled=True ), [(1, 'a'),(2, 'b'),(3, 'c'),(4, 'a')])\n", - "test_eq(t.zip(cycled=False), [(1, 'a'),(2, 'b'),(3, 'c')])" + "test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt), [0,2]) # 1<2, 3<1 fails, 2<3\n", + "test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt, negate=True), [1])" ] }, { "cell_type": "code", "execution_count": null, - "id": "9422eb7a", - "metadata": {}, + "id": "3e7fa556", + "metadata": { + "time_run": "2025-12-20T04:01:07.229418+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def map_zip(self:L, f, *args, cycled=False, **kwargs):\n", - " \"Combine `zip` and `starmap`\"\n", - " return self.zip(cycled=cycled).starmap(f, *args, **kwargs)" + "@curryable\n", + "def argfirst(self:L, f, negate=False):\n", + " \"Return index of first matching item\"\n", + " if negate: f = not_(f)\n", + " return first(i for i,o in self.enumerate() if f(o))" ] }, { "cell_type": "code", "execution_count": null, - "id": "6b41b405", - "metadata": {}, + "id": "eee16654", + "metadata": { + "time_run": "2025-12-20T04:01:10.881530+00:00" + }, "outputs": [], "source": [ - "t = L([1,2,3],[2,3,4])\n", - "test_eq(t.map_zip(operator.mul), [2,6,12])" + "test_eq(t.argfirst(lambda o:o>4), 5)\n", + "test_eq(t.argfirst(lambda o:o>4,negate=True),0)" + ] + }, + { + "cell_type": "markdown", + "id": "e645edea", + "metadata": {}, + "source": [ + "Curried `L.argfirst` returns a partial function that finds the index of the first matching item in any iterable. This is useful when mapping over nested collections to find the first match in each." ] }, { "cell_type": "code", "execution_count": null, - "id": "75969588", - "metadata": {}, + "id": "500f6cfc", + "metadata": { + "time_run": "2025-12-20T04:02:45.057050+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#3) [2,1,None]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested = L([[1,2,8,4], [5,9,7], [1,1,1]])\n", + "nested.map(L.argfirst(gt(5)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90509c05", + "metadata": { + "time_run": "2025-12-20T04:02:53.166312+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def zipwith(self:L, *rest, cycled=False):\n", - " \"Create new `L` with `self` zip with each of `*rest`\"\n", - " return self._new([self, *rest]).zip(cycled=cycled)" + "@curryable\n", + "def starargfirst(self:L, f, negate=False):\n", + " \"Like `argfirst`, but unpacks elements as args to `f`\"\n", + " _f = lambda x: f(*x)\n", + " if negate: _f = not_(_f)\n", + " return first(i for i,o in self.enumerate() if _f(o))" + ] + }, + { + "cell_type": "markdown", + "id": "8ff3d6cc", + "metadata": {}, + "source": [ + "`L.starargfirst` is like `argfirst`, but unpacks tuple elements as arguments to the predicate (and is curryable):" ] }, { "cell_type": "code", "execution_count": null, - "id": "33e21037", - "metadata": {}, + "id": "d4044d5c", + "metadata": { + "time_run": "2025-12-20T04:03:01.253303+00:00" + }, "outputs": [], "source": [ - "b = [[0],[1],[2,2]]\n", - "t = L([1,2,3]).zipwith(b)\n", - "test_eq(t, [(1,[0]), (2,[1]), (3,[2,2])])" + "test_eq(L((3,1),(1,2),(2,3)).starargfirst(lt), 1)\n", + "test_eq(L((1,2),(3,1),(2,3)).starargfirst(lt, negate=True), 1)" ] }, { "cell_type": "code", "execution_count": null, - "id": "89eca591", - "metadata": {}, + "id": "dc43b9b9", + "metadata": { + "time_run": "2025-12-20T04:03:04.068854+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", - "def map_zipwith(self:L, f, *rest, cycled=False, **kwargs):\n", - " \"Combine `zipwith` and `starmap`\"\n", - " return self.zipwith(*rest, cycled=cycled).starmap(f, **kwargs)" + "@curryable\n", + "def rstarargfirst(self:L, f, negate=False):\n", + " \"Like `starargfirst`, but reverse the order of args\"\n", + " _f = lambda x: f(*x[::-1])\n", + " if negate: _f = not_(_f)\n", + " return first(i for i,o in self.enumerate() if _f(o))" + ] + }, + { + "cell_type": "markdown", + "id": "3aa280a5", + "metadata": {}, + "source": [ + "`L.rstarargfirst` is like `starargfirst`, but reverses the order of unpacked arguments (and is curryable):" ] }, { "cell_type": "code", "execution_count": null, - "id": "1af11e0e", - "metadata": {}, + "id": "8dd7abc2", + "metadata": { + "time_run": "2025-12-20T04:03:10.079786+00:00" + }, "outputs": [], "source": [ - "test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12])" + "test_eq(L((1,3),(2,1),(3,2)).rstarargfirst(lt), 1) # 3<1 fails, 1<2\n", + "test_eq(L((2,1),(1,3),(3,2)).rstarargfirst(lt, negate=True), 1)" ] }, { "cell_type": "code", "execution_count": null, "id": "44055cd0", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:03:12.822770+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -1938,10 +2674,13 @@ { "cell_type": "code", "execution_count": null, - "id": "561a2f1a", - "metadata": {}, + "id": "89a13d92", + "metadata": { + "time_run": "2025-12-20T04:04:05.419820+00:00" + }, "outputs": [], "source": [ + "t = L([['x', [0]], ['y', [1]], ['z', [2,2]]])\n", "test_eq(t.itemgot(1), b)" ] }, @@ -1949,7 +2688,9 @@ "cell_type": "code", "execution_count": null, "id": "10f3d35e", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:10.084667+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -1963,7 +2704,9 @@ "cell_type": "code", "execution_count": null, "id": "dad8526e", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:10.889701+00:00" + }, "outputs": [], "source": [ "# Example when items are not a dict\n", @@ -1979,11 +2722,14 @@ "cell_type": "code", "execution_count": null, "id": "9f238dfb", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:12.840996+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def sorted(self:L, key=None, reverse=False, cmp=None, **kwargs):\n", " \"New `L` sorted by `key`, using `sort_ex`. If key is str use `attrgetter`; if int use `itemgetter`\"\n", " return self._new(sorted_ex(self, key=key, reverse=reverse, cmp=cmp, **kwargs))" @@ -1993,21 +2739,58 @@ "cell_type": "code", "execution_count": null, "id": "646cbc22", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:13.983474+00:00" + }, "outputs": [], "source": [ "test_eq(L(a).sorted('a').attrgot('b'), [2,4])" ] }, + { + "cell_type": "markdown", + "id": "271c0790", + "metadata": {}, + "source": [ + "Curried `L.sorted` returns a partial function that sorts any iterable by the given key. This is useful when mapping a sort operation over nested collections—each inner collection gets sorted independently using the same key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ab61e1e", + "metadata": { + "time_run": "2025-12-20T04:04:30.241478+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#2) [[(1, 'a'), (2, 'b'), (3, 'c')],[(4, 'd'), (6, 'f')]]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested = L([[(3,'c'),(1,'a'),(2,'b')], [(6,'f'),(4,'d')]])\n", + "nested.map(L.sorted(0))" + ] + }, { "cell_type": "code", "execution_count": null, "id": "1c21919b", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:38.209251+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def starsorted(self:L, key, reverse=False):\n", " \"Like `sorted`, but unpacks elements as args to `key`\"\n", " return self._new(sorted(self, key=lambda x: key(*x), reverse=reverse))" @@ -2025,7 +2808,9 @@ "cell_type": "code", "execution_count": null, "id": "74a73a56", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:39.571284+00:00" + }, "outputs": [], "source": [ "test_eq(L((3,1),(1,2),(2,0)).starsorted(operator.sub), [(1,2),(3,1),(2,0)]) # sorted by a-b: 2, 2, -1\n", @@ -2036,11 +2821,14 @@ "cell_type": "code", "execution_count": null, "id": "7549144e", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:40.833412+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def rstarsorted(self:L, key, reverse=False):\n", " \"Like `starsorted`, but reverse the order of args\"\n", " return self._new(sorted(self, key=lambda x: key(*x[::-1]), reverse=reverse))" @@ -2058,7 +2846,9 @@ "cell_type": "code", "execution_count": null, "id": "7123a1a2", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:43.552945+00:00" + }, "outputs": [], "source": [ "test_eq(L((1,3),(2,1),(0,2)).rstarsorted(operator.sub), [(2,1),(1,3),(0,2)]) # sorted by b-a: 0, 2, 2\n", @@ -2069,7 +2859,9 @@ "cell_type": "code", "execution_count": null, "id": "9d86b896", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:44.978415+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -2083,7 +2875,9 @@ "cell_type": "code", "execution_count": null, "id": "6c74fb84", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:45.602601+00:00" + }, "outputs": [], "source": [ "test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))" @@ -2093,7 +2887,9 @@ "cell_type": "code", "execution_count": null, "id": "b493aad4", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:46.553116+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -2107,7 +2903,9 @@ "cell_type": "code", "execution_count": null, "id": "b4aaf8fa", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:46.985070+00:00" + }, "outputs": [], "source": [ "t = L([0,1,2,3],4,L(5,6)).copy()\n", @@ -2118,7 +2916,9 @@ "cell_type": "code", "execution_count": null, "id": "7b2691d6", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:47.920823+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -2142,7 +2942,9 @@ "cell_type": "code", "execution_count": null, "id": "3edd22b5", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:49.088023+00:00" + }, "outputs": [], "source": [ "t = L(1,2,3,4,5)\n", @@ -2155,11 +2957,14 @@ "cell_type": "code", "execution_count": null, "id": "7b83c9e9", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:50.819228+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def reduce(self:L, f, initial=None):\n", " \"Wrapper for `functools.reduce`\"\n", " return reduce(f, self) if initial is None else reduce(f, self, initial)" @@ -2169,22 +2974,59 @@ "cell_type": "code", "execution_count": null, "id": "bd4184ca", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:04:51.555525+00:00" + }, "outputs": [], "source": [ "test_eq(L(1,2,3,4).reduce(operator.add), 10)\n", "test_eq(L(1,2,3,4).reduce(operator.mul, 10), 240)" ] }, + { + "cell_type": "markdown", + "id": "a5f70806", + "metadata": {}, + "source": [ + "Curried `L.reduce` returns a partial function that reduces any iterable using the given function. This is useful when mapping a reduction over nested collections—each inner collection gets reduced independently using the same operation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65b58b72", + "metadata": { + "time_run": "2025-12-20T04:05:07.066752+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#3) [6,9,30]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested = L([[1,2,3], [4,5], [6,7,8,9]])\n", + "nested.map(L.reduce(operator.add))" + ] + }, { "cell_type": "code", "execution_count": null, "id": "3db27a6a", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:11.807533+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def starreduce(self:L, f, initial=None):\n", " \"Like `reduce`, but unpacks elements as args to `f`\"\n", " _f = lambda acc, x: f(acc, *x)\n", @@ -2203,7 +3045,9 @@ "cell_type": "code", "execution_count": null, "id": "41fd24f2", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:13.051839+00:00" + }, "outputs": [], "source": [ "test_eq(L((1,2),(3,4),(5,6)).starreduce(lambda acc,a,b: acc+a*b, 0), 44) # 0+1*2+3*4+5*6\n", @@ -2222,7 +3066,9 @@ "cell_type": "code", "execution_count": null, "id": "dfe97eec", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:14.700414+00:00" + }, "outputs": [ { "data": { @@ -2230,7 +3076,7 @@ "44" ] }, - "execution_count": null, + "execution_count": 0, "metadata": {}, "output_type": "execute_result" } @@ -2244,11 +3090,14 @@ "cell_type": "code", "execution_count": null, "id": "046f01fb", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:15.579612+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def rstarreduce(self:L, f, initial=None):\n", " \"Like `starreduce`, but reverse the order of unpacked args\"\n", " _f = lambda acc, x: f(acc, *x[::-1])\n", @@ -2267,7 +3116,9 @@ "cell_type": "code", "execution_count": null, "id": "84b3413b", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:17.131820+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -2281,7 +3132,9 @@ "cell_type": "code", "execution_count": null, "id": "18e6c336", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:17.810384+00:00" + }, "outputs": [], "source": [ "test_eq(L(1,2,3,4).sum(), 10)\n", @@ -2292,7 +3145,9 @@ "cell_type": "code", "execution_count": null, "id": "2a96a0fd", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:18.572205+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -2306,7 +3161,9 @@ "cell_type": "code", "execution_count": null, "id": "a41eb5c0", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:19.203479+00:00" + }, "outputs": [], "source": [ "test_eq(L(1,2,3,4).product(), 24)\n", @@ -2317,7 +3174,9 @@ "cell_type": "code", "execution_count": null, "id": "f82d9975", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:19.901175+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -2331,7 +3190,9 @@ "cell_type": "code", "execution_count": null, "id": "6d29b6c6", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:20.859782+00:00" + }, "outputs": [], "source": [ "t = L(0,1,2,3)\n", @@ -2342,7 +3203,9 @@ "cell_type": "code", "execution_count": null, "id": "1366f0fa", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:22.590955+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -2356,7 +3219,9 @@ "cell_type": "code", "execution_count": null, "id": "ca54000c", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:24.225471+00:00" + }, "outputs": [], "source": [ "t = L(SimpleNamespace(),SimpleNamespace())\n", @@ -2376,7 +3241,9 @@ "cell_type": "code", "execution_count": null, "id": "9c927236", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:27.551072+00:00" + }, "outputs": [], "source": [ "#| export\n", @@ -2398,7 +3265,9 @@ "cell_type": "code", "execution_count": null, "id": "816b9e9c", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:28.956194+00:00" + }, "outputs": [], "source": [ "test_eq(list(itertools.islice(L(1,2,3).cycle(), 7)), [1,2,3,1,2,3,1])" @@ -2408,11 +3277,14 @@ "cell_type": "code", "execution_count": null, "id": "79e84e6d", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:30.367974+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def takewhile(self:L, f):\n", " \"Same as `itertools.takewhile`\"\n", " return self._new(itertools.takewhile(f, self))" @@ -2430,22 +3302,59 @@ "cell_type": "code", "execution_count": null, "id": "6b80039c", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:05:33.087764+00:00" + }, "outputs": [], "source": [ "test_eq(L(1,2,3,4,5,1,2).takewhile(lambda x: x<4), [1,2,3])\n", "test_eq(L(1,2,3,11).takewhile(lt(10)), [1,2,3])" ] }, + { + "cell_type": "markdown", + "id": "1e20f6b1", + "metadata": {}, + "source": [ + "Curried `L.takewhile` returns a partial function that takes elements from the beginning of any iterable while the predicate holds. This is useful when mapping over nested collections—each inner collection gets truncated at the first failing element using the same predicate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da17275d", + "metadata": { + "time_run": "2025-12-20T04:05:49.346082+00:00" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(#3) [[1, 2],[2, 3],[]]" + ] + }, + "execution_count": 0, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "nested = L([[1,2,5,3], [2,3,8,1], [9,1,2]])\n", + "nested.map(L.takewhile(lt(5)))" + ] + }, { "cell_type": "code", "execution_count": null, "id": "c9f6e354", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:06:04.969811+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def dropwhile(self:L, f):\n", " \"Same as `itertools.dropwhile`\"\n", " return self._new(itertools.dropwhile(f, self))" @@ -2463,7 +3372,9 @@ "cell_type": "code", "execution_count": null, "id": "086eec4a", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:06:06.119039+00:00" + }, "outputs": [], "source": [ "test_eq(L(1,2,3,4,5,1,2).dropwhile(lt(4)), [4,5,1,2])\n", @@ -2474,11 +3385,14 @@ "cell_type": "code", "execution_count": null, "id": "45f04c5c", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:06:06.905151+00:00" + }, "outputs": [], "source": [ "#| export\n", "@patch\n", + "@curryable\n", "def startakewhile(self:L, f):\n", " \"Like `takewhile`, but unpacks elements as args to `f`\"\n", " return self._new(itertools.takewhile(lambda x: f(*x), self))" @@ -2496,22 +3410,51 @@ "cell_type": "code", "execution_count": null, "id": "14007659", - "metadata": {}, + "metadata": { + "time_run": "2025-12-20T04:06:09.899298+00:00" + }, "outputs": [], "source": [ "test_eq(L((1,2),(2,3),(4,1),(5,6)).startakewhile(lambda a,b: a Date: Sat, 20 Dec 2025 14:35:34 +1000 Subject: [PATCH 180/182] clean --- nbs/02_foundation.ipynb | 1070 ++++++++++----------------------------- 1 file changed, 270 insertions(+), 800 deletions(-) diff --git a/nbs/02_foundation.ipynb b/nbs/02_foundation.ipynb index 831f6c9f..b11c3e01 100644 --- a/nbs/02_foundation.ipynb +++ b/nbs/02_foundation.ipynb @@ -4,9 +4,7 @@ "cell_type": "code", "execution_count": null, "id": "50bae063", - "metadata": { - "time_run": "2025-12-20T03:49:43.240601+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| default_exp foundation" @@ -16,9 +14,7 @@ "cell_type": "code", "execution_count": null, "id": "0f974791", - "metadata": { - "time_run": "2025-12-20T03:49:43.242546+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -35,9 +31,7 @@ "cell_type": "code", "execution_count": null, "id": "8990087b", - "metadata": { - "time_run": "2025-12-20T03:49:43.244467+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| hide\n", @@ -60,9 +54,7 @@ { "cell_type": "markdown", "id": "13ac17ff", - "metadata": { - "heading_collapsed": true - }, + "metadata": {}, "source": [ "## Foundational Functions" ] @@ -71,10 +63,7 @@ "cell_type": "code", "execution_count": null, "id": "f059df0f", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:43.295505+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -91,10 +80,7 @@ "cell_type": "code", "execution_count": null, "id": "c3fd027f", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:43.297043+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -115,9 +101,7 @@ { "cell_type": "markdown", "id": "620dc00e", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "`add_docs` allows you to add docstrings to a class and its associated methods. This function allows you to group docstrings together seperate from your code, which enables you to define one-line functions as well as organize your code more succintly. We believe this confers a number of benefits which we discuss in [our style guide](https://docs.fast.ai/dev/style.html).\n", "\n", @@ -128,10 +112,7 @@ "cell_type": "code", "execution_count": null, "id": "0832c567", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:43.378519+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "class T:\n", @@ -142,9 +123,7 @@ { "cell_type": "markdown", "id": "adedd47e", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "You can add documentation to this class like so:" ] @@ -153,10 +132,7 @@ "cell_type": "code", "execution_count": null, "id": "da9d95f9", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:43.789054+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "add_docs(T, cls_doc=\"A docstring for the class.\",\n", @@ -167,9 +143,7 @@ { "cell_type": "markdown", "id": "a91ee046", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "Now, docstrings will appear as expected:" ] @@ -178,10 +152,7 @@ "cell_type": "code", "execution_count": null, "id": "2c4a26aa", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:44.103306+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(T.__doc__, \"A docstring for the class.\")\n", @@ -192,9 +163,7 @@ { "cell_type": "markdown", "id": "47545904", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "`add_docs` also validates that all of your public methods contain a docstring. If one of your methods is not documented, it will raise an error:" ] @@ -203,10 +172,7 @@ "cell_type": "code", "execution_count": null, "id": "a9f77cf6", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:44.194964+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "class T:\n", @@ -221,10 +187,7 @@ "cell_type": "code", "execution_count": null, "id": "88805b49", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:44.276162+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| hide\n", @@ -243,10 +206,7 @@ "cell_type": "code", "execution_count": null, "id": "b180313a", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:44.313472+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -259,9 +219,7 @@ { "cell_type": "markdown", "id": "e25bd40f", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "Instead of using `add_docs`, you can use the decorator `docs` as shown below. Note that the docstring for the class can be set with the argument `cls_doc`:" ] @@ -270,10 +228,7 @@ "cell_type": "code", "execution_count": null, "id": "d10f59f9", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:44.417345+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "@docs\n", @@ -294,9 +249,7 @@ { "cell_type": "markdown", "id": "2ad622b1", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "For either the `docs` decorator or the `add_docs` function, you can still define your docstrings in the normal way. Below we set the docstring for the class as usual, but define the method docstrings through the `_docs` attribute:" ] @@ -305,10 +258,7 @@ "cell_type": "code", "execution_count": null, "id": "e4bbd1b9", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:44.539508+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "@docs\n", @@ -326,10 +276,7 @@ "cell_type": "code", "execution_count": null, "id": "0b1f9829", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:44.543215+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -348,7 +295,7 @@ "*Test whether `o` can be used in a `for` loop*" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -361,10 +308,7 @@ "cell_type": "code", "execution_count": null, "id": "4e191b5b", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.130433+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "assert is_iter([1])\n", @@ -377,10 +321,7 @@ "cell_type": "code", "execution_count": null, "id": "34a29b43", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.134924+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -393,9 +334,7 @@ { "cell_type": "markdown", "id": "198216f8", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "`coll_repr` is used to provide a more informative [`__repr__`](https://stackoverflow.com/questions/1984162/purpose-of-pythons-repr) about list-like objects. `coll_repr` and is used by `L` to build a `__repr__` that displays the length of a list in addition to a preview of a list.\n", "\n", @@ -406,10 +345,7 @@ "cell_type": "code", "execution_count": null, "id": "fe3ed954", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.192522+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(coll_repr(range(1000),10), '(#1000) [0,1,2,3,4,5,6,7,8,9...]')\n", @@ -422,10 +358,7 @@ "cell_type": "code", "execution_count": null, "id": "6cf96f49", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.195740+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -438,10 +371,7 @@ "cell_type": "code", "execution_count": null, "id": "66a28a53", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.198886+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -460,10 +390,7 @@ "cell_type": "code", "execution_count": null, "id": "3b7dd826", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.234542+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(mask2idxs([False,True,False,True]), [1,3])\n", @@ -475,10 +402,7 @@ "cell_type": "code", "execution_count": null, "id": "af754019", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.237172+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -492,10 +416,7 @@ "cell_type": "code", "execution_count": null, "id": "7f8805d7", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.239023+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(itertools.islice(cycle([1,2,3]),5), [1,2,3,1,2])\n", @@ -508,10 +429,7 @@ "cell_type": "code", "execution_count": null, "id": "9346b91a", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.240645+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -524,10 +442,7 @@ "cell_type": "code", "execution_count": null, "id": "21c554a1", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.242243+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(zip_cycle([1,2,3,4],list('abc')), [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'a')])" @@ -537,10 +452,7 @@ "cell_type": "code", "execution_count": null, "id": "8a281173", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.276669+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -552,9 +464,7 @@ { "cell_type": "markdown", "id": "2c4497d9", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "You can, for example index a single item in a list with an integer or a 0-dimensional numpy array:" ] @@ -563,10 +473,7 @@ "cell_type": "code", "execution_count": null, "id": "cf94cb54", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.278649+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "assert is_indexer(1)\n", @@ -576,9 +483,7 @@ { "cell_type": "markdown", "id": "504d89e8", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "However, you cannot index into single item in a list with another list or a numpy array with ndim > 0. " ] @@ -587,10 +492,7 @@ "cell_type": "code", "execution_count": null, "id": "1159335e", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.280124+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "assert not is_indexer([1, 2])\n", @@ -601,10 +503,7 @@ "cell_type": "code", "execution_count": null, "id": "3fd3133f", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.281676+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -617,10 +516,7 @@ "cell_type": "code", "execution_count": null, "id": "73a52d33", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.283204+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -628,7 +524,7 @@ "60" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -641,10 +537,7 @@ "cell_type": "code", "execution_count": null, "id": "fa5990de", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.285240+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -652,7 +545,7 @@ "1" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -665,10 +558,7 @@ "cell_type": "code", "execution_count": null, "id": "6f9ef4e6", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.332834+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -676,7 +566,7 @@ "0" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -688,9 +578,7 @@ { "cell_type": "markdown", "id": "d8a026cf", - "metadata": { - "heading_collapsed": true - }, + "metadata": {}, "source": [ "## `L` helpers" ] @@ -699,10 +587,7 @@ "cell_type": "code", "execution_count": null, "id": "eefd2763", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.334989+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -720,9 +605,7 @@ { "cell_type": "markdown", "id": "0ec47e38", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "`ColBase` is a base class that emulates the functionality of a python `list`:" ] @@ -731,10 +614,7 @@ "cell_type": "code", "execution_count": null, "id": "896956c4", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.336439+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "class _T(CollBase): pass\n", @@ -759,9 +639,7 @@ "cell_type": "code", "execution_count": null, "id": "d953eaf9", - "metadata": { - "time_run": "2025-12-20T03:49:46.337965+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -775,9 +653,7 @@ "cell_type": "code", "execution_count": null, "id": "f571f489", - "metadata": { - "time_run": "2025-12-20T03:49:46.388886+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -838,9 +714,7 @@ "cell_type": "code", "execution_count": null, "id": "05e35e65", - "metadata": { - "time_run": "2025-12-20T03:49:46.390290+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -855,9 +729,7 @@ "cell_type": "code", "execution_count": null, "id": "68e52115", - "metadata": { - "time_run": "2025-12-20T03:49:46.392040+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -865,7 +737,7 @@ "__main__.L" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -886,9 +758,7 @@ { "cell_type": "markdown", "id": "f523de6f", - "metadata": { - "heading_collapsed": true - }, + "metadata": {}, "source": [ "### Examples and overview" ] @@ -897,10 +767,7 @@ "cell_type": "code", "execution_count": null, "id": "a8a78e40", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.393916+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "from fastcore.utils import gt" @@ -909,9 +776,7 @@ { "cell_type": "markdown", "id": "40393b8d", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "Read [this overview section](https://fastcore.fast.ai/tour.html#L) for a quick tutorial of `L`, as well as background on the name. \n", "\n", @@ -922,10 +787,7 @@ "cell_type": "code", "execution_count": null, "id": "72125daf", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.395404+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -933,7 +795,7 @@ "(#12) [0,1,2,'j',4,'k',6,7,8,9,10,11]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -954,9 +816,7 @@ { "cell_type": "markdown", "id": "694445fa", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "Any `L` is a `Sequence` so you can use it with methods like `random.sample`:" ] @@ -965,10 +825,7 @@ "cell_type": "code", "execution_count": null, "id": "03129273", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:46.452847+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "assert isinstance(t, Sequence)" @@ -978,10 +835,7 @@ "cell_type": "code", "execution_count": null, "id": "26bd9f4d", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.007291+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "import random" @@ -991,10 +845,7 @@ "cell_type": "code", "execution_count": null, "id": "6786d495", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.189117+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1002,7 +853,7 @@ "[6, 11, 0]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1016,10 +867,7 @@ "cell_type": "code", "execution_count": null, "id": "3212eb40", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.301175+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| hide\n", @@ -1036,9 +884,7 @@ { "cell_type": "markdown", "id": "7a91eab9", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "There are optimized indexers for arrays, tensors, and DataFrames." ] @@ -1047,10 +893,7 @@ "cell_type": "code", "execution_count": null, "id": "7de0e777", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.375193+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "import pandas as pd" @@ -1060,10 +903,7 @@ "cell_type": "code", "execution_count": null, "id": "2f32ddb9", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.381016+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "arr = np.arange(9).reshape(3,3)\n", @@ -1078,9 +918,7 @@ { "cell_type": "markdown", "id": "4bf55f8a", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "You can also modify an `L` with `append`, `+`, and `*`." ] @@ -1089,10 +927,7 @@ "cell_type": "code", "execution_count": null, "id": "8fc4ca2e", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.484732+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L()\n", @@ -1114,9 +949,7 @@ { "cell_type": "markdown", "id": "1c498ee5", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "An `L` can be constructed from anything iterable, although tensors and arrays will not be iterated over on construction, unless you pass `use_list` to the constructor." ] @@ -1125,10 +958,7 @@ "cell_type": "code", "execution_count": null, "id": "c9d8144c", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.587852+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L([1,2,3]),[1,2,3])\n", @@ -1146,9 +976,7 @@ { "cell_type": "markdown", "id": "6b378062", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "If `match` is not `None` then the created list is same len as `match`, either by:\n", "\n", @@ -1160,10 +988,7 @@ "cell_type": "code", "execution_count": null, "id": "75d2daf4", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.636301+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(1,match=[1,2,3]),[1,1,1])\n", @@ -1174,9 +999,7 @@ { "cell_type": "markdown", "id": "c8153cf0", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "If you create an `L` from an existing `L` then you'll get back the original object (since `L` uses the `NewChkMeta` metaclass)." ] @@ -1185,10 +1008,7 @@ "cell_type": "code", "execution_count": null, "id": "706d5ac7", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.641477+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_is(L(t), t)" @@ -1197,9 +1017,7 @@ { "cell_type": "markdown", "id": "89d2c804", - "metadata": { - "hidden": 1 - }, + "metadata": {}, "source": [ "An `L` is considred equal to a list if they have the same elements. It's never considered equal to a `str` a `set` or a `dict` even if they have the same elements/keys." ] @@ -1208,10 +1026,7 @@ "cell_type": "code", "execution_count": null, "id": "a208d40c", - "metadata": { - "hidden": 1, - "time_run": "2025-12-20T03:49:47.721016+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(['a', 'b']), ['a', 'b'])\n", @@ -1231,9 +1046,7 @@ "cell_type": "code", "execution_count": null, "id": "8ba7bfe3", - "metadata": { - "time_run": "2025-12-20T03:49:47.762838+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1254,7 +1067,7 @@ "*Retrieve `idx` (can be list of indices, or mask, or int) items*" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1267,9 +1080,7 @@ "cell_type": "code", "execution_count": null, "id": "a6edf562", - "metadata": { - "time_run": "2025-12-20T03:49:48.042534+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L(range(12))\n", @@ -1284,9 +1095,7 @@ "cell_type": "code", "execution_count": null, "id": "93ffea3b", - "metadata": { - "time_run": "2025-12-20T03:49:48.091917+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1307,7 +1116,7 @@ "*Set `idx` (can be list of indices, or mask, or int) items to `o` (which is broadcast if not iterable)*" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1320,9 +1129,7 @@ "cell_type": "code", "execution_count": null, "id": "b84c56e3", - "metadata": { - "time_run": "2025-12-20T03:49:48.134862+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t[4,6] = 0\n", @@ -1335,9 +1142,7 @@ "cell_type": "code", "execution_count": null, "id": "d966b525", - "metadata": { - "time_run": "2025-12-20T03:49:48.141605+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1351,9 +1156,7 @@ "cell_type": "code", "execution_count": null, "id": "62aa9a1a", - "metadata": { - "time_run": "2025-12-20T03:49:48.148221+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(4,1,2,3,4,4).unique(), [4,1,2,3])" @@ -1363,9 +1166,7 @@ "cell_type": "code", "execution_count": null, "id": "840aef37", - "metadata": { - "time_run": "2025-12-20T03:49:48.154657+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1379,9 +1180,7 @@ "cell_type": "code", "execution_count": null, "id": "76dd07e3", - "metadata": { - "time_run": "2025-12-20T03:49:48.161156+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(1,2,3).val2idx(), {3:2,1:0,2:1})" @@ -1391,9 +1190,7 @@ "cell_type": "code", "execution_count": null, "id": "f2e038cc", - "metadata": { - "time_run": "2025-12-20T03:49:48.167659+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1407,9 +1204,7 @@ "cell_type": "code", "execution_count": null, "id": "f7d0109a", - "metadata": { - "time_run": "2025-12-20T03:49:48.172271+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq_type(L.range([1,1,1]), L(range(3)))\n", @@ -1420,9 +1215,7 @@ "cell_type": "code", "execution_count": null, "id": "d1d16238", - "metadata": { - "time_run": "2025-12-20T04:00:47.965252+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1436,9 +1229,7 @@ "cell_type": "code", "execution_count": null, "id": "4592473d", - "metadata": { - "time_run": "2025-12-20T04:00:48.374391+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L('a','b','c').enumerate(), [(0,'a'),(1,'b'),(2,'c')])" @@ -1448,9 +1239,7 @@ "cell_type": "code", "execution_count": null, "id": "ccd5c96c", - "metadata": { - "time_run": "2025-12-20T04:00:48.891136+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1464,9 +1253,7 @@ "cell_type": "code", "execution_count": null, "id": "3fd03f76", - "metadata": { - "time_run": "2025-12-20T04:00:49.403500+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L('a','b','c').renumerate(), [('a', 0), ('b', 1), ('c', 2)])" @@ -1476,9 +1263,7 @@ "cell_type": "code", "execution_count": null, "id": "13795373", - "metadata": { - "time_run": "2025-12-20T03:49:48.181438+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1500,9 +1285,7 @@ "cell_type": "code", "execution_count": null, "id": "5b9583f3", - "metadata": { - "time_run": "2025-12-20T03:49:48.188171+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L.split('a b c'), ['a','b','c'])\n", @@ -1514,9 +1297,7 @@ "cell_type": "code", "execution_count": null, "id": "6da7b43c", - "metadata": { - "time_run": "2025-12-20T03:49:48.195107+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1538,9 +1319,7 @@ "cell_type": "code", "execution_count": null, "id": "2508910d", - "metadata": { - "time_run": "2025-12-20T03:49:48.203068+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L.splitlines('a\\nb\\nc'), ['a','b','c'])\n", @@ -1551,9 +1330,7 @@ "cell_type": "code", "execution_count": null, "id": "826ffa88", - "metadata": { - "time_run": "2025-12-20T03:49:48.210412+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1593,9 +1370,7 @@ "cell_type": "code", "execution_count": null, "id": "0d6407c6", - "metadata": { - "time_run": "2025-12-20T03:49:48.217512+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1610,9 +1385,7 @@ "cell_type": "code", "execution_count": null, "id": "566326ab", - "metadata": { - "time_run": "2025-12-20T03:49:48.224338+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L.range(4).map(operator.neg), [0,-1,-2,-3])" @@ -1630,9 +1403,7 @@ "cell_type": "code", "execution_count": null, "id": "24680aae", - "metadata": { - "time_run": "2025-12-20T03:49:48.231016+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L.range(4).map('#{}#'), ['#0#','#1#','#2#','#3#'])" @@ -1650,9 +1421,7 @@ "cell_type": "code", "execution_count": null, "id": "57657757", - "metadata": { - "time_run": "2025-12-20T03:49:48.236175+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L.range(4).map(list('abcd')), list('abcd'))" @@ -1670,9 +1439,7 @@ "cell_type": "code", "execution_count": null, "id": "4d14fb0f", - "metadata": { - "time_run": "2025-12-20T03:49:48.244732+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "def f(a=None,b=None): return b\n", @@ -1683,9 +1450,7 @@ "cell_type": "code", "execution_count": null, "id": "9c5a4633", - "metadata": { - "time_run": "2025-12-20T03:49:48.251205+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1706,9 +1471,7 @@ "cell_type": "code", "execution_count": null, "id": "047fc5bf", - "metadata": { - "time_run": "2025-12-20T03:49:48.257555+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1716,7 +1479,7 @@ "(#3) [['1', '2', '3'],['4', '5', '6'],['7', '8', '9']]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1733,9 +1496,7 @@ { "cell_type": "markdown", "id": "4cb739bc", - "metadata": { - "use_thinking": true - }, + "metadata": {}, "source": [ "As mentioned in the `curryable` discussion, `map` can be curried. This can work well together with `L.splitlines` output:" ] @@ -1744,9 +1505,7 @@ "cell_type": "code", "execution_count": null, "id": "d3f55753", - "metadata": { - "time_run": "2025-12-20T03:49:48.263191+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1754,7 +1513,7 @@ "(#3) [[1, 2, 3],[4, 5, 6],[7, 8, 9]]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1776,9 +1535,7 @@ "cell_type": "code", "execution_count": null, "id": "6665dc80", - "metadata": { - "time_run": "2025-12-20T03:50:09.736968+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1788,7 +1545,7 @@ " [7, 8, 9]])" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1801,9 +1558,7 @@ "cell_type": "code", "execution_count": null, "id": "ce5b413b", - "metadata": { - "time_run": "2025-12-20T03:50:12.040162+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1824,9 +1579,7 @@ "cell_type": "code", "execution_count": null, "id": "51b78c23", - "metadata": { - "time_run": "2025-12-20T03:50:13.701721+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1834,7 +1587,7 @@ "(#2) [['a', 'b', 'c'],['d', 'e']]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1847,9 +1600,7 @@ "cell_type": "code", "execution_count": null, "id": "d2680cbd", - "metadata": { - "time_run": "2025-12-20T03:50:15.049900+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1864,9 +1615,7 @@ "cell_type": "code", "execution_count": null, "id": "d0db8cbb", - "metadata": { - "time_run": "2025-12-20T03:50:15.952094+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "words = L.split('aaa abc bba')\n", @@ -1885,9 +1634,7 @@ "cell_type": "code", "execution_count": null, "id": "f7669c31", - "metadata": { - "time_run": "2025-12-20T03:50:17.817264+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1895,7 +1642,7 @@ "(#2) [{'a': ['a1', 'a3'], 'b': ['b2']},{'x': ['x1', 'x3'], 'y': ['y2']}]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1908,9 +1655,7 @@ "cell_type": "code", "execution_count": null, "id": "37dc972a", - "metadata": { - "time_run": "2025-12-20T03:50:22.681900+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -1933,9 +1678,7 @@ "cell_type": "code", "execution_count": null, "id": "45316c68", - "metadata": { - "time_run": "2025-12-20T03:50:42.002132+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L([(1,2),(3,4)]).starmap(operator.add), [3,7])\n", @@ -1954,9 +1697,7 @@ "cell_type": "code", "execution_count": null, "id": "0acf95c8", - "metadata": { - "time_run": "2025-12-20T03:52:05.851645+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -1964,7 +1705,7 @@ "(#2) [[2, 12],[30, 56]]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1978,9 +1719,7 @@ "cell_type": "code", "execution_count": null, "id": "cb2283b0", - "metadata": { - "time_run": "2025-12-20T03:52:24.774589+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2003,9 +1742,7 @@ "cell_type": "code", "execution_count": null, "id": "5c03c922", - "metadata": { - "time_run": "2025-12-20T03:52:27.186565+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((1,2),(3,4)).rstarmap(operator.sub), [1,1]) # 2-1, 4-3\n", @@ -2024,9 +1761,7 @@ "cell_type": "code", "execution_count": null, "id": "9c3815ac", - "metadata": { - "time_run": "2025-12-20T03:52:56.445761+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -2034,7 +1769,7 @@ "(#2) [['1x', '2y'],['3z']]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -2048,9 +1783,7 @@ "cell_type": "code", "execution_count": null, "id": "c0846f2e", - "metadata": { - "time_run": "2025-12-20T03:53:02.196257+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2064,9 +1797,7 @@ "cell_type": "code", "execution_count": null, "id": "92f098d8", - "metadata": { - "time_run": "2025-12-20T03:53:03.062053+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(range(1,5)).map_dict(), {1:1, 2:2, 3:3, 4:4})\n", @@ -2077,9 +1808,7 @@ "cell_type": "code", "execution_count": null, "id": "105ac114", - "metadata": { - "time_run": "2025-12-20T03:53:56.747629+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2093,9 +1822,7 @@ "cell_type": "code", "execution_count": null, "id": "b9883e0c", - "metadata": { - "time_run": "2025-12-20T03:53:57.398783+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L([[1,2,3],'abc'])\n", @@ -2106,9 +1833,7 @@ "cell_type": "code", "execution_count": null, "id": "702912aa", - "metadata": { - "time_run": "2025-12-20T03:53:58.299915+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L([[1,2,3,4],['a','b','c']])\n", @@ -2120,9 +1845,7 @@ "cell_type": "code", "execution_count": null, "id": "6340b2b3", - "metadata": { - "time_run": "2025-12-20T03:54:59.890978+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2136,9 +1859,7 @@ "cell_type": "code", "execution_count": null, "id": "d645b000", - "metadata": { - "time_run": "2025-12-20T03:55:00.603864+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L([1,2,3],[2,3,4])\n", @@ -2149,9 +1870,7 @@ "cell_type": "code", "execution_count": null, "id": "bc012fad", - "metadata": { - "time_run": "2025-12-20T03:55:08.964459+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2165,9 +1884,7 @@ "cell_type": "code", "execution_count": null, "id": "f10ffe4f", - "metadata": { - "time_run": "2025-12-20T03:55:09.737642+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "b = [[0],[1],[2,2]]\n", @@ -2179,9 +1896,7 @@ "cell_type": "code", "execution_count": null, "id": "5a40226f", - "metadata": { - "time_run": "2025-12-20T03:55:12.144197+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2195,9 +1910,7 @@ "cell_type": "code", "execution_count": null, "id": "bf653b2b", - "metadata": { - "time_run": "2025-12-20T03:55:12.736535+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(1,2,3).map_zipwith(operator.mul, [2,3,4]), [2,6,12])" @@ -2207,9 +1920,7 @@ "cell_type": "code", "execution_count": null, "id": "66e76d03", - "metadata": { - "time_run": "2025-12-20T03:56:48.552651+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2224,9 +1935,7 @@ "cell_type": "code", "execution_count": null, "id": "669d3a5c", - "metadata": { - "time_run": "2025-12-20T03:56:49.207492+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L(range(12))\n", @@ -2246,9 +1955,7 @@ "cell_type": "code", "execution_count": null, "id": "616fed64", - "metadata": { - "time_run": "2025-12-20T03:57:06.416582+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -2256,7 +1963,7 @@ "(#3) [[1, 2, 3],[4, 5, 6],[7, 8, 9]]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -2269,9 +1976,7 @@ "cell_type": "code", "execution_count": null, "id": "d1735243", - "metadata": { - "time_run": "2025-12-20T03:57:01.213394+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -2279,7 +1984,7 @@ "(#3) [[],[5, 6],[7, 8, 9]]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -2292,9 +1997,7 @@ "cell_type": "code", "execution_count": null, "id": "085b690b", - "metadata": { - "time_run": "2025-12-20T03:57:10.630220+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2319,9 +2022,7 @@ "cell_type": "code", "execution_count": null, "id": "cb878281", - "metadata": { - "time_run": "2025-12-20T03:57:12.890479+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((1,2),(3,1),(2,3)).starfilter(lt), [(1,2),(2,3)])\n", @@ -2340,9 +2041,7 @@ "cell_type": "code", "execution_count": null, "id": "e939f0d3", - "metadata": { - "time_run": "2025-12-20T03:58:42.289932+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -2350,7 +2049,7 @@ "(#2) [[(1, 5)],[(4, 6)]]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -2364,9 +2063,7 @@ "cell_type": "code", "execution_count": null, "id": "e0fa80df", - "metadata": { - "time_run": "2025-12-20T03:58:48.417077+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2391,9 +2088,7 @@ "cell_type": "code", "execution_count": null, "id": "a26c1ac4", - "metadata": { - "time_run": "2025-12-20T03:59:03.561807+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((2,1),(1,3),(3,2)).rstarfilter(lt), [(2,1),(3,2)]) # 1<2, 3<1 fails, 2<3\n", @@ -2404,9 +2099,7 @@ "cell_type": "code", "execution_count": null, "id": "a2515ab8", - "metadata": { - "time_run": "2025-12-20T03:59:04.220635+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2421,9 +2114,7 @@ "cell_type": "code", "execution_count": null, "id": "6f637a43", - "metadata": { - "time_run": "2025-12-20T03:59:53.723656+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L([0,1,2,3,4,99,0])\n", @@ -2434,9 +2125,7 @@ "cell_type": "code", "execution_count": null, "id": "cf22acd4", - "metadata": { - "time_run": "2025-12-20T04:00:06.155115+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2461,9 +2150,7 @@ "cell_type": "code", "execution_count": null, "id": "a4766805", - "metadata": { - "time_run": "2025-12-20T04:00:19.931053+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((1,2),(3,1),(2,3)).starargwhere(lt), [0,2])\n", @@ -2474,9 +2161,7 @@ "cell_type": "code", "execution_count": null, "id": "1f010f9c", - "metadata": { - "time_run": "2025-12-20T04:00:20.396811+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2501,9 +2186,7 @@ "cell_type": "code", "execution_count": null, "id": "155730fd", - "metadata": { - "time_run": "2025-12-20T04:00:23.542229+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((2,1),(1,3),(3,2)).rstarargwhere(lt), [0,2]) # 1<2, 3<1 fails, 2<3\n", @@ -2514,9 +2197,7 @@ "cell_type": "code", "execution_count": null, "id": "3e7fa556", - "metadata": { - "time_run": "2025-12-20T04:01:07.229418+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2532,9 +2213,7 @@ "cell_type": "code", "execution_count": null, "id": "eee16654", - "metadata": { - "time_run": "2025-12-20T04:01:10.881530+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(t.argfirst(lambda o:o>4), 5)\n", @@ -2553,9 +2232,7 @@ "cell_type": "code", "execution_count": null, "id": "500f6cfc", - "metadata": { - "time_run": "2025-12-20T04:02:45.057050+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -2563,7 +2240,7 @@ "(#3) [2,1,None]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -2577,9 +2254,7 @@ "cell_type": "code", "execution_count": null, "id": "90509c05", - "metadata": { - "time_run": "2025-12-20T04:02:53.166312+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2604,9 +2279,7 @@ "cell_type": "code", "execution_count": null, "id": "d4044d5c", - "metadata": { - "time_run": "2025-12-20T04:03:01.253303+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((3,1),(1,2),(2,3)).starargfirst(lt), 1)\n", @@ -2617,9 +2290,7 @@ "cell_type": "code", "execution_count": null, "id": "dc43b9b9", - "metadata": { - "time_run": "2025-12-20T04:03:04.068854+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2644,9 +2315,7 @@ "cell_type": "code", "execution_count": null, "id": "8dd7abc2", - "metadata": { - "time_run": "2025-12-20T04:03:10.079786+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((1,3),(2,1),(3,2)).rstarargfirst(lt), 1) # 3<1 fails, 1<2\n", @@ -2657,9 +2326,7 @@ "cell_type": "code", "execution_count": null, "id": "44055cd0", - "metadata": { - "time_run": "2025-12-20T04:03:12.822770+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2675,9 +2342,7 @@ "cell_type": "code", "execution_count": null, "id": "89a13d92", - "metadata": { - "time_run": "2025-12-20T04:04:05.419820+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L([['x', [0]], ['y', [1]], ['z', [2,2]]])\n", @@ -2688,9 +2353,7 @@ "cell_type": "code", "execution_count": null, "id": "10f3d35e", - "metadata": { - "time_run": "2025-12-20T04:04:10.084667+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2704,9 +2367,7 @@ "cell_type": "code", "execution_count": null, "id": "dad8526e", - "metadata": { - "time_run": "2025-12-20T04:04:10.889701+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "# Example when items are not a dict\n", @@ -2722,9 +2383,7 @@ "cell_type": "code", "execution_count": null, "id": "9f238dfb", - "metadata": { - "time_run": "2025-12-20T04:04:12.840996+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2739,9 +2398,7 @@ "cell_type": "code", "execution_count": null, "id": "646cbc22", - "metadata": { - "time_run": "2025-12-20T04:04:13.983474+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(a).sorted('a').attrgot('b'), [2,4])" @@ -2759,9 +2416,7 @@ "cell_type": "code", "execution_count": null, "id": "1ab61e1e", - "metadata": { - "time_run": "2025-12-20T04:04:30.241478+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -2769,7 +2424,7 @@ "(#2) [[(1, 'a'), (2, 'b'), (3, 'c')],[(4, 'd'), (6, 'f')]]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -2783,9 +2438,7 @@ "cell_type": "code", "execution_count": null, "id": "1c21919b", - "metadata": { - "time_run": "2025-12-20T04:04:38.209251+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2808,9 +2461,7 @@ "cell_type": "code", "execution_count": null, "id": "74a73a56", - "metadata": { - "time_run": "2025-12-20T04:04:39.571284+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((3,1),(1,2),(2,0)).starsorted(operator.sub), [(1,2),(3,1),(2,0)]) # sorted by a-b: 2, 2, -1\n", @@ -2821,9 +2472,7 @@ "cell_type": "code", "execution_count": null, "id": "7549144e", - "metadata": { - "time_run": "2025-12-20T04:04:40.833412+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2846,9 +2495,7 @@ "cell_type": "code", "execution_count": null, "id": "7123a1a2", - "metadata": { - "time_run": "2025-12-20T04:04:43.552945+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((1,3),(2,1),(0,2)).rstarsorted(operator.sub), [(2,1),(1,3),(0,2)]) # sorted by b-a: 0, 2, 2\n", @@ -2859,9 +2506,7 @@ "cell_type": "code", "execution_count": null, "id": "9d86b896", - "metadata": { - "time_run": "2025-12-20T04:04:44.978415+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2875,9 +2520,7 @@ "cell_type": "code", "execution_count": null, "id": "6c74fb84", - "metadata": { - "time_run": "2025-12-20T04:04:45.602601+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L([0,1,2,3],4,L(5,6)).concat(), range(7))" @@ -2887,9 +2530,7 @@ "cell_type": "code", "execution_count": null, "id": "b493aad4", - "metadata": { - "time_run": "2025-12-20T04:04:46.553116+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2903,9 +2544,7 @@ "cell_type": "code", "execution_count": null, "id": "b4aaf8fa", - "metadata": { - "time_run": "2025-12-20T04:04:46.985070+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L([0,1,2,3],4,L(5,6)).copy()\n", @@ -2916,9 +2555,7 @@ "cell_type": "code", "execution_count": null, "id": "7b2691d6", - "metadata": { - "time_run": "2025-12-20T04:04:47.920823+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2942,9 +2579,7 @@ "cell_type": "code", "execution_count": null, "id": "3edd22b5", - "metadata": { - "time_run": "2025-12-20T04:04:49.088023+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L(1,2,3,4,5)\n", @@ -2957,9 +2592,7 @@ "cell_type": "code", "execution_count": null, "id": "7b83c9e9", - "metadata": { - "time_run": "2025-12-20T04:04:50.819228+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -2974,9 +2607,7 @@ "cell_type": "code", "execution_count": null, "id": "bd4184ca", - "metadata": { - "time_run": "2025-12-20T04:04:51.555525+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(1,2,3,4).reduce(operator.add), 10)\n", @@ -2995,9 +2626,7 @@ "cell_type": "code", "execution_count": null, "id": "65b58b72", - "metadata": { - "time_run": "2025-12-20T04:05:07.066752+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -3005,7 +2634,7 @@ "(#3) [6,9,30]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -3019,9 +2648,7 @@ "cell_type": "code", "execution_count": null, "id": "3db27a6a", - "metadata": { - "time_run": "2025-12-20T04:05:11.807533+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3045,9 +2672,7 @@ "cell_type": "code", "execution_count": null, "id": "41fd24f2", - "metadata": { - "time_run": "2025-12-20T04:05:13.051839+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((1,2),(3,4),(5,6)).starreduce(lambda acc,a,b: acc+a*b, 0), 44) # 0+1*2+3*4+5*6\n", @@ -3066,9 +2691,7 @@ "cell_type": "code", "execution_count": null, "id": "dfe97eec", - "metadata": { - "time_run": "2025-12-20T04:05:14.700414+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -3076,7 +2699,7 @@ "44" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -3090,9 +2713,7 @@ "cell_type": "code", "execution_count": null, "id": "046f01fb", - "metadata": { - "time_run": "2025-12-20T04:05:15.579612+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3116,9 +2737,7 @@ "cell_type": "code", "execution_count": null, "id": "84b3413b", - "metadata": { - "time_run": "2025-12-20T04:05:17.131820+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3132,9 +2751,7 @@ "cell_type": "code", "execution_count": null, "id": "18e6c336", - "metadata": { - "time_run": "2025-12-20T04:05:17.810384+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(1,2,3,4).sum(), 10)\n", @@ -3145,9 +2762,7 @@ "cell_type": "code", "execution_count": null, "id": "2a96a0fd", - "metadata": { - "time_run": "2025-12-20T04:05:18.572205+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3161,9 +2776,7 @@ "cell_type": "code", "execution_count": null, "id": "a41eb5c0", - "metadata": { - "time_run": "2025-12-20T04:05:19.203479+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(1,2,3,4).product(), 24)\n", @@ -3174,9 +2787,7 @@ "cell_type": "code", "execution_count": null, "id": "f82d9975", - "metadata": { - "time_run": "2025-12-20T04:05:19.901175+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3190,9 +2801,7 @@ "cell_type": "code", "execution_count": null, "id": "6d29b6c6", - "metadata": { - "time_run": "2025-12-20T04:05:20.859782+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L(0,1,2,3)\n", @@ -3203,9 +2812,7 @@ "cell_type": "code", "execution_count": null, "id": "1366f0fa", - "metadata": { - "time_run": "2025-12-20T04:05:22.590955+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3219,9 +2826,7 @@ "cell_type": "code", "execution_count": null, "id": "ca54000c", - "metadata": { - "time_run": "2025-12-20T04:05:24.225471+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "t = L(SimpleNamespace(),SimpleNamespace())\n", @@ -3241,9 +2846,7 @@ "cell_type": "code", "execution_count": null, "id": "9c927236", - "metadata": { - "time_run": "2025-12-20T04:05:27.551072+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3265,9 +2868,7 @@ "cell_type": "code", "execution_count": null, "id": "816b9e9c", - "metadata": { - "time_run": "2025-12-20T04:05:28.956194+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(list(itertools.islice(L(1,2,3).cycle(), 7)), [1,2,3,1,2,3,1])" @@ -3277,9 +2878,7 @@ "cell_type": "code", "execution_count": null, "id": "79e84e6d", - "metadata": { - "time_run": "2025-12-20T04:05:30.367974+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3302,9 +2901,7 @@ "cell_type": "code", "execution_count": null, "id": "6b80039c", - "metadata": { - "time_run": "2025-12-20T04:05:33.087764+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(1,2,3,4,5,1,2).takewhile(lambda x: x<4), [1,2,3])\n", @@ -3323,9 +2920,7 @@ "cell_type": "code", "execution_count": null, "id": "da17275d", - "metadata": { - "time_run": "2025-12-20T04:05:49.346082+00:00" - }, + "metadata": {}, "outputs": [ { "data": { @@ -3333,7 +2928,7 @@ "(#3) [[1, 2],[2, 3],[]]" ] }, - "execution_count": 0, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -3347,9 +2942,7 @@ "cell_type": "code", "execution_count": null, "id": "c9f6e354", - "metadata": { - "time_run": "2025-12-20T04:06:04.969811+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3372,9 +2965,7 @@ "cell_type": "code", "execution_count": null, "id": "086eec4a", - "metadata": { - "time_run": "2025-12-20T04:06:06.119039+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L(1,2,3,4,5,1,2).dropwhile(lt(4)), [4,5,1,2])\n", @@ -3385,9 +2976,7 @@ "cell_type": "code", "execution_count": null, "id": "45f04c5c", - "metadata": { - "time_run": "2025-12-20T04:06:06.905151+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "#| export\n", @@ -3410,9 +2999,7 @@ "cell_type": "code", "execution_count": null, "id": "14007659", - "metadata": { - "time_run": "2025-12-20T04:06:09.899298+00:00" - }, + "metadata": {}, "outputs": [], "source": [ "test_eq(L((1,2),(2,3),(4,1),(5,6)).startakewhile(lambda a,b: a Date: Sat, 20 Dec 2025 14:35:47 +1000 Subject: [PATCH 181/182] release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca281a9d..a117f347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ +## 1.9.4 + +### New Features + +- Added loads of curried methods to `L` ([#720](https://github.com/AnswerDotAI/fastcore/issues/720)) +- Include stderr in `run` exception details ([#719](https://github.com/AnswerDotAI/fastcore/issues/719)) + + ## 1.9.3 ### New Features From d90257fee0e24d9818bbd108c75a470d280cdc55 Mon Sep 17 00:00:00 2001 From: Jeremy Howard Date: Sat, 20 Dec 2025 14:35:59 +1000 Subject: [PATCH 182/182] bump --- fastcore/__init__.py | 2 +- settings.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcore/__init__.py b/fastcore/__init__.py index 84452039..96974ae1 100644 --- a/fastcore/__init__.py +++ b/fastcore/__init__.py @@ -1 +1 @@ -__version__ = "1.9.4" +__version__ = "1.9.5" diff --git a/settings.ini b/settings.ini index b991843a..00e0080a 100644 --- a/settings.ini +++ b/settings.ini @@ -8,7 +8,7 @@ author = Jeremy Howard and Sylvain Gugger author_email = infos@fast.ai copyright = fast.ai branch = main -version = 1.9.4 +version = 1.9.5 min_python = 3.10 audience = Developers language = English