From b64404907bfb8ffc2b291a9e5f890c08809f52ff Mon Sep 17 00:00:00 2001 From: Kerem Turgutlu Date: Thu, 4 Jun 2026 17:45:16 +0300 Subject: [PATCH] fixes #823 --- fastcore/xtras.py | 30 +-- nbs/03_xtras.ipynb | 461 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 423 insertions(+), 68 deletions(-) diff --git a/fastcore/xtras.py b/fastcore/xtras.py index d9117277..0a99d7d2 100644 --- a/fastcore/xtras.py +++ b/fastcore/xtras.py @@ -5,16 +5,16 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_xtras.ipynb. # %% auto #0 -__all__ = ['spark_chars', 'UNSET', 'walk_join', 'walk', 'exttypes', 'globtastic', 'pglob', 'maybe_open', 'mkdir', 'image_size', +__all__ = ['UNSET', 'spark_chars', 'walk_join', 'walk', 'exttypes', 'globtastic', 'pglob', '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', - 'atomic_save', 'load_mod', 'import_no_init', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', 'mapped', - 'IterLen', 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', 'frontmatter', - 'clean_cli_output', 'unqid', 'rtoken_hex', 'friendly_name', 'n_friendly_names', 'exec_eval', + 'atomic_save', 'load_mod', 'import_no_init', 'Unset', 'dict2obj', 'obj2dict', 'repr_dict', 'is_listy', + 'mapped', 'IterLen', 'ReindexCollection', 'SaveReturn', 'trim_wraps', 'save_iter', 'asave_iter', + 'frontmatter', 'clean_cli_output', 'unqid', 'rtoken_hex', 'friendly_name', 'n_friendly_names', 'exec_eval', 'get_source_link', 'sparkline', 'modify_exception', 'round_multiple', 'set_num_threads', 'join_path_file', 'autostart', 'EventTimer', 'stringfmt_names', 'PartialFormatter', 'partial_format', 'truncstr', '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', + 'dataclass_src', 'nullable_dc', 'make_nullable', 'flexiclass', 'asdict', 'vars_pub', 'is_typeddict', 'is_namedtuple', 'CachedIter', 'CachedAwaitable', 'reawaitable', 'is_async_callable', 'maybe_await', 'to_aiter', 'maybe_aiter', 'mapa', 'noopa', 'flexicache', 'time_policy', 'mtime_policy', 'timed_cache'] @@ -449,10 +449,18 @@ def import_no_init(name): if len(parts)>1: path = path.parent.joinpath(*parts[1:]).with_suffix('.py') return load_mod(name, path) +# %% ../nbs/03_xtras.ipynb #6368c600 +class Unset(Enum): + _Unset='' + def __repr__(self): return 'UNSET' + def __str__ (self): return 'UNSET' + def __bool__(self): return False +UNSET = Unset._Unset + # %% ../nbs/03_xtras.ipynb #9579358d -def dict2obj(d=None, list_func=L, dict_func=AttrDict, **kwargs): +def dict2obj(d=UNSET, list_func=L, dict_func=AttrDict, **kwargs): "Convert (possibly nested) dicts (or lists of dicts) to `AttrDict`" - if d is None: d={} + if d is UNSET: d={} if isinstance(d, (L,list)): return list_func([dict2obj(v, list_func=list_func, dict_func=dict_func) for v in d]) if not isinstance(d, dict): return d return dict_func(**{k:dict2obj(v, list_func=list_func, dict_func=dict_func) for k,v in (d|kwargs).items()}) @@ -946,14 +954,6 @@ def dataclass_src(cls): src += f" {f.name}: {type2str(f.type)}{d}\n" return src -# %% ../nbs/03_xtras.ipynb #ecd7f0a2 -class Unset(Enum): - _Unset='' - def __repr__(self): return 'UNSET' - def __str__ (self): return 'UNSET' - def __bool__(self): return False -UNSET = Unset._Unset - # %% ../nbs/03_xtras.ipynb #1e9a46ab def nullable_dc(cls): "Like `dataclass`, but default of `UNSET` added to fields without defaults" diff --git a/nbs/03_xtras.ipynb b/nbs/03_xtras.ipynb index 69f6879c..10abb02b 100644 --- a/nbs/03_xtras.ipynb +++ b/nbs/03_xtras.ipynb @@ -1049,7 +1049,7 @@ { "data": { "text/plain": [ - "'pip 26.1.1 from /Users/jhoward/aai-ws/.venv/lib/python3.12/site-packages/pip (python 3.12)'" + "'pip 26.1.2 from /Users/keremturgutlu/aai-ws/.venv/lib/python3.12/site-packages/pip (python 3.12)'" ] }, "execution_count": null, @@ -1446,7 +1446,7 @@ { "data": { "text/plain": [ - "ModuleSpec(name='fastcore.basics', loader=<_frozen_importlib_external.SourceFileLoader object>, origin='/Users/jhoward/aai-ws/fastcore/fastcore/basics.py')" + "ModuleSpec(name='fastcore.basics', loader=<_frozen_importlib_external.SourceFileLoader object>, origin='/Users/keremturgutlu/aai-ws/fastcore/fastcore/basics.py')" ] }, "execution_count": null, @@ -1520,6 +1520,22 @@ "## Collections" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "6368c600", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class Unset(Enum):\n", + " _Unset=''\n", + " def __repr__(self): return 'UNSET'\n", + " def __str__ (self): return 'UNSET'\n", + " def __bool__(self): return False\n", + "UNSET = Unset._Unset" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1528,9 +1544,9 @@ "outputs": [], "source": [ "#| export\n", - "def dict2obj(d=None, list_func=L, dict_func=AttrDict, **kwargs):\n", + "def dict2obj(d=UNSET, list_func=L, dict_func=AttrDict, **kwargs):\n", " \"Convert (possibly nested) dicts (or lists of dicts) to `AttrDict`\"\n", - " if d is None: d={}\n", + " if d is UNSET: d={}\n", " if isinstance(d, (L,list)): return list_func([dict2obj(v, list_func=list_func, dict_func=dict_func) for v in d])\n", " if not isinstance(d, dict): return d\n", " return dict_func(**{k:dict2obj(v, list_func=list_func, dict_func=dict_func) for k,v in (d|kwargs).items()})" @@ -1597,6 +1613,25 @@ "test_eq(ds[0].b.c, 2)" ] }, + { + "cell_type": "markdown", + "id": "567c28ab", + "metadata": {}, + "source": [ + "None values should be preserved:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51298c0b", + "metadata": {}, + "outputs": [], + "source": [ + "d3 = dict2obj(a=1, b=dict(c=2,d=None))\n", + "test_eq(d3.b['d'], None)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1832,7 +1867,7 @@ { "data": { "text/plain": [ - "Path('/Users/jhoward/aai-ws/fastcore/fastcore')" + "Path('/Users/keremturgutlu/aai-ws/fastcore/fastcore')" ] }, "execution_count": null, @@ -2132,12 +2167,22 @@ "id": "37757e70", "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/keremturgutlu/aai-ws/fasthtml/fasthtml/jupyter.py:136: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.\n", + " from starlette.testclient import TestClient\n" + ] + }, { "data": { "text/markdown": [ + "
\n", + "\n", "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L544){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L567){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "#### ReindexCollection\n", "\n", @@ -2150,7 +2195,9 @@ "\n", "```\n", "\n", - "*Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`*" + "*Reindexes collection `coll` with indices `idxs` and optional LRU cache of size `cache`*\n", + "\n", + "
" ], "text/plain": [ "def ReindexCollection(\n", @@ -2225,9 +2272,11 @@ { "data": { "text/markdown": [ + "
\n", + "\n", "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L555){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L578){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "###### ReindexCollection.reindex\n", "\n", @@ -2240,7 +2289,9 @@ "\n", "```\n", "\n", - "*Replace `self.idxs` with idxs*" + "*Replace `self.idxs` with idxs*\n", + "\n", + "
" ], "text/plain": [ "def reindex(\n", @@ -2332,9 +2383,11 @@ { "data": { "text/markdown": [ + "
\n", + "\n", "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L559){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L582){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.cache_clear\n", "\n", @@ -2347,7 +2400,9 @@ "\n", "```\n", "\n", - "*Clear LRU cache*" + "*Clear LRU cache*\n", + "\n", + "
" ], "text/plain": [ "def cache_clear(\n", @@ -2401,9 +2456,11 @@ { "data": { "text/markdown": [ + "
\n", + "\n", "---\n", "\n", - "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L556){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L579){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", "\n", "##### ReindexCollection.shuffle\n", "\n", @@ -2416,7 +2473,9 @@ "\n", "```\n", "\n", - "*Randomly shuffle indices*" + "*Randomly shuffle indices*\n", + "\n", + "
" ], "text/plain": [ "def shuffle(\n", @@ -2451,7 +2510,7 @@ { "data": { "text/plain": [ - "['f', 'd', 'g', 'a', 'e', 'h', 'b', 'c']" + "['h', 'b', 'c', 'd', 'f', 'a', 'g', 'e']" ] }, "execution_count": null, @@ -2934,7 +2993,7 @@ { "data": { "text/plain": [ - "'_AxLfeLvbTlSFLGRe4ldOzA'" + "'_1eWDbWtXRyCDSMNSEbTMNw'" ] }, "execution_count": null, @@ -3029,7 +3088,7 @@ { "data": { "text/plain": [ - "('7dbf2cd2', '8c7d7247')" + "('741ab73d', '8c7d7247')" ] }, "execution_count": null, @@ -3433,7 +3492,7 @@ { "data": { "text/plain": [ - "'https://github.com/AnswerDotAI/fastcorefastcore/test.py#L48'" + "'https://github.com/AnswerDotAI/fastcorefastcore/test.py#L50'" ] }, "execution_count": null, @@ -3720,7 +3779,43 @@ "execution_count": null, "id": "9420f9d8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/markdown": [ + "
\n", + "\n", + "---\n", + "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L801){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### EventTimer\n", + "\n", + "```python\n", + "\n", + "def EventTimer(\n", + " store:int=5, span:int=60\n", + "):\n", + "\n", + "\n", + "```\n", + "\n", + "*An event timer with history of `store` items of time `span`*\n", + "\n", + "
" + ], + "text/plain": [ + "def EventTimer(\n", + " store:int=5, span:int=60\n", + "):\n", + "\"\"\"An event timer with history of `store` items of time `span`\"\"\"" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "show_doc(EventTimer, title_level=4)" ] @@ -3738,7 +3833,16 @@ "execution_count": null, "id": "d5d39cff", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Num Events: 1, Freq/sec: 145.4\n", + "Most recent: ▅▇▁▆▇ 257.7 291.9 205.3 274.4 294.1\n" + ] + } + ], "source": [ "# Random wait function for testing\n", "def _randwait(): yield from (sleep(random.random()/200) for _ in range(100))\n", @@ -3813,7 +3917,43 @@ "execution_count": null, "id": "d1bababa", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/markdown": [ + "
\n", + "\n", + "---\n", + "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L833){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### PartialFormatter\n", + "\n", + "```python\n", + "\n", + "def PartialFormatter(\n", + " \n", + "):\n", + "\n", + "\n", + "```\n", + "\n", + "*A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args*\n", + "\n", + "
" + ], + "text/plain": [ + "def PartialFormatter(\n", + " \n", + "):\n", + "\"\"\"A `string.Formatter` that doesn't error on missing fields, and tracks missing fields and unused args\"\"\"" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "show_doc(PartialFormatter, title_level=4)" ] @@ -3920,7 +4060,15 @@ "execution_count": null, "id": "d1278d6c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2000-01-01 12:00:00 UTC is 2000-01-01 14:00:00+02:00 local time\n" + ] + } + ], "source": [ "dt = datetime(2000,1,1,12)\n", "print(f'{dt} UTC is {utc2local(dt)} local time')" @@ -3944,7 +4092,15 @@ "execution_count": null, "id": "8756e202", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2000-01-01 12:00:00 local is 2000-01-01 10:00:00+00:00 UTC time\n" + ] + } + ], "source": [ "print(f'{dt} local is {local2utc(dt)} UTC time')" ] @@ -4044,7 +4200,43 @@ "execution_count": null, "id": "423b8c58", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/markdown": [ + "
\n", + "\n", + "---\n", + "\n", + "[source](https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/xtras.py#L896){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### ContextManagers\n", + "\n", + "```python\n", + "\n", + "def ContextManagers(\n", + " mgrs\n", + "):\n", + "\n", + "\n", + "```\n", + "\n", + "*Wrapper for `contextlib.ExitStack` which enters a collection of context managers*\n", + "\n", + "
" + ], + "text/plain": [ + "def ContextManagers(\n", + " mgrs\n", + "):\n", + "\"\"\"Wrapper for `contextlib.ExitStack` which enters a collection of context managers\"\"\"" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "show_doc(ContextManagers, title_level=4)" ] @@ -4114,7 +4306,29 @@ "execution_count": null, "id": "abf99c3f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/markdown": [ + "
\n", + "\n", + "```html\n", + "a child\n", + "```\n", + "\n", + "
" + ], + "text/plain": [ + "Markdown(```html\n", + "a child\n", + "```)" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "hl_md('a child')" ] @@ -4169,28 +4383,25 @@ "execution_count": null, "id": "58908987", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "@dataclass\n", + "class DC:\n", + " x: int\n", + " y: Union[float, None] = None\n", + " z: float = None\n", + "\n" + ] + } + ], "source": [ "DC = make_dataclass('DC', [('x', int), ('y', Optional[float], None), ('z', float, None)])\n", "print(dataclass_src(DC))" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "ecd7f0a2", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "class Unset(Enum):\n", - " _Unset=''\n", - " def __repr__(self): return 'UNSET'\n", - " def __str__ (self): return 'UNSET'\n", - " def __bool__(self): return False\n", - "UNSET = Unset._Unset" - ] - }, { "cell_type": "code", "execution_count": null, @@ -4211,7 +4422,18 @@ "execution_count": null, "id": "c7d70e0c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Person(name='Bob', age=UNSET, city='Unknown')" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@nullable_dc\n", "class Person: name: str; age: int; city: str = \"Unknown\"\n", @@ -4253,7 +4475,18 @@ "execution_count": null, "id": "72b22fa8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Person(name='Bob', age=UNSET, city='NY')" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@dataclass\n", "class Person: name: str; age: int; city: str = \"Unknown\"\n", @@ -4267,7 +4500,18 @@ "execution_count": null, "id": "19516b10", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Person(name='Bob', age=UNSET, city='Unknown')" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "Person(name=\"Bob\")" ] @@ -4277,7 +4521,18 @@ "execution_count": null, "id": "bef2318a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Person(name='Bob', age=34, city='Unknown')" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "Person(\"Bob\", 34)" ] @@ -4314,7 +4569,18 @@ "execution_count": null, "id": "9fd419f3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Person(name='Bob', age=UNSET, city='Unknown')" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@flexiclass\n", "class Person: name: str; age: int; city: str = \"Unknown\"\n", @@ -4336,7 +4602,18 @@ "execution_count": null, "id": "5f74eaf5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Person(name='Bob', age=UNSET, city='Unknown')" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "class Person: name: str; age: int; city: str = \"Unknown\"\n", "\n", @@ -4358,7 +4635,18 @@ "execution_count": null, "id": "0a22024f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "class Person: name: str; age: int; city: str = \"Unknown\"\n", "\n", @@ -4401,7 +4689,18 @@ "execution_count": null, "id": "4056c9d1", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'name': 'Bob', 'city': 'Unknown'}" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "asdict(bob)" ] @@ -4569,7 +4868,25 @@ "execution_count": null, "id": "1fdc8e07", "metadata": {}, - "outputs": [], + "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", @@ -4624,7 +4941,16 @@ "execution_count": null, "id": "a7336c4d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "data\n", + "data\n" + ] + } + ], "source": [ "@reawaitable\n", "async def fetch_data():\n", @@ -4908,7 +5234,18 @@ "execution_count": null, "id": "e16e7b41", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@flexicache(time_policy(10), mtime_policy('000_tour.ipynb'))\n", "def cached_func(x, y): return x+y\n", @@ -4921,7 +5258,25 @@ "execution_count": null, "id": "62e3eef3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n" + ] + }, + { + "data": { + "text/plain": [ + "3" + ] + }, + "execution_count": null, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "@flexicache(time_policy(10), mtime_policy('000_tour.ipynb'))\n", "async def cached_func(x, y): return x+y\n",