From 3323ce9ec7477c6b1ab428677fb669d0c8c17b66 Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Tue, 8 Oct 2024 14:47:18 -0500 Subject: [PATCH 1/5] Add support for type casting in the typed decorator --- fastcore/basics.py | 55 +++++++++++------- nbs/01_basics.ipynb | 134 +++++++++++++++++++++++++++----------------- 2 files changed, 119 insertions(+), 70 deletions(-) diff --git a/fastcore/basics.py b/fastcore/basics.py index 94d5f07f..79b83178 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -4,11 +4,11 @@ # %% auto 0 __all__ = ['defaults', 'null', 'num_methods', 'rnum_methods', 'inum_methods', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4', 'Self', - 'ifnone', 'maybe_attr', 'basic_repr', 'is_array', 'listify', 'tuplify', 'true', 'NullType', 'tonull', - 'get_class', 'mk_class', 'wrap_class', 'ignore_exceptions', 'exec_local', 'risinstance', 'ver2tuple', 'Inf', - 'in_', 'ret_true', 'ret_false', 'stop', 'gen', 'chunked', 'otherwise', 'custom_dir', 'AttrDict', - 'AttrDictDefault', 'NS', 'get_annotations_ex', 'eval_type', 'type_hints', 'annotations', 'anno_ret', - 'signature_ex', 'union2tuple', 'argnames', 'with_cast', 'store_attr', 'attrdict', 'properties', + 'type_map', 'ifnone', 'maybe_attr', 'basic_repr', 'is_array', 'listify', 'tuplify', 'true', 'NullType', + 'tonull', 'get_class', 'mk_class', 'wrap_class', 'ignore_exceptions', 'exec_local', 'risinstance', + 'ver2tuple', 'Inf', 'in_', 'ret_true', 'ret_false', 'stop', 'gen', 'chunked', 'otherwise', 'custom_dir', + 'AttrDict', 'AttrDictDefault', 'NS', 'get_annotations_ex', 'eval_type', 'type_hints', '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', @@ -1134,21 +1134,36 @@ def add_props(f, g=None, n=2): def _typeerr(arg, val, typ): return TypeError(f"{arg}=={val} not {typ}") # %% ../nbs/01_basics.ipynb -def typed(f): - "Decorator to check param and return types at runtime" - names = f.__code__.co_varnames - anno = annotations(f) - ret = anno.pop('return',None) - def _f(*args,**kwargs): - kw = {**kwargs} - if len(anno) > 0: - for i,arg in enumerate(args): kw[names[i]] = arg - for k,v in kw.items(): - if k in anno and not isinstance(v,anno[k]): raise _typeerr(k, v, anno[k]) - res = f(*args,**kwargs) - if ret is not None and not isinstance(res,ret): raise _typeerr("return", res, ret) - return res - return functools.update_wrapper(_f, f) +type_map = {int: int, float: float, str: str, bool: bool} + +# %% ../nbs/01_basics.ipynb +def typed(_func=None, *, cast=False): + "Decorator to check param and return types at runtime, with optional casting" + def decorator(f): + names = f.__code__.co_varnames + anno = annotations(f) + ret = anno.pop('return', None) + def _f(*args, **kwargs): + kw = {**kwargs} + if len(anno) > 0: + for i,arg in enumerate(args): kw[names[i]] = arg + for k,v in kw.items(): + if k in anno: + expected_type = anno[k] + if cast: + try: kw[k] = type_map[expected_type](v) + except (ValueError, TypeError) as e: raise _typeerr(k, v, expected_type) from e + elif not isinstance(v, expected_type): raise _typeerr(k, v, expected_type) + res = f(**kw) + if ret is not None: + if cast: + try: res = type_map[ret](res) + except (ValueError, TypeError) as e: raise _typeerr("return", res, ret) from e + elif not isinstance(res, ret): raise _typeerr("return", res, ret) + return res + return functools.update_wrapper(_f, f) + if _func is None: return decorator # Decorator was called with arguments + else: return decorator(_func) # Decorator was called without arguments # %% ../nbs/01_basics.ipynb def exec_new(code): diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index e8ae8a5e..bc08bbd4 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": [ @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -55,7 +55,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -65,7 +65,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -84,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -94,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -113,7 +113,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -125,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -151,7 +151,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -160,7 +160,7 @@ "'__main__.SomeClass()'" ] }, - "execution_count": 33, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -179,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -188,7 +188,7 @@ "\"__main__.SomeClass(a=1, b='foo')\"" ] }, - "execution_count": 34, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -212,7 +212,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -221,7 +221,7 @@ "\"__main__.AnotherClass(c=__main__.SomeClass(a=1, b='foo'), d='bar')\"" ] }, - "execution_count": 35, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -244,7 +244,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -253,7 +253,7 @@ "\"__main__.SomeClass(a=1, b='foo')\"" ] }, - "execution_count": 36, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -268,7 +268,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -6305,21 +6305,43 @@ "outputs": [], "source": [ "#|export\n", - "def typed(f):\n", - " \"Decorator to check param and return types at runtime\"\n", - " names = f.__code__.co_varnames\n", - " anno = annotations(f)\n", - " ret = anno.pop('return',None)\n", - " def _f(*args,**kwargs):\n", - " kw = {**kwargs}\n", - " if len(anno) > 0:\n", - " for i,arg in enumerate(args): kw[names[i]] = arg\n", - " for k,v in kw.items():\n", - " if k in anno and not isinstance(v,anno[k]): raise _typeerr(k, v, anno[k])\n", - " res = f(*args,**kwargs)\n", - " if ret is not None and not isinstance(res,ret): raise _typeerr(\"return\", res, ret)\n", - " return res\n", - " return functools.update_wrapper(_f, f)" + "type_map = {int: int, float: float, str: str, bool: bool}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|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", + " names = f.__code__.co_varnames\n", + " anno = annotations(f)\n", + " ret = anno.pop('return', None)\n", + " def _f(*args, **kwargs):\n", + " kw = {**kwargs}\n", + " if len(anno) > 0:\n", + " for i,arg in enumerate(args): kw[names[i]] = arg\n", + " for k,v in kw.items():\n", + " if k in anno:\n", + " expected_type = anno[k]\n", + " if cast:\n", + " try: kw[k] = type_map[expected_type](v)\n", + " except (ValueError, TypeError) as e: raise _typeerr(k, v, expected_type) from e\n", + " elif not isinstance(v, expected_type): raise _typeerr(k, v, expected_type)\n", + " res = f(**kw)\n", + " if ret is not None:\n", + " if cast:\n", + " try: res = type_map[ret](res)\n", + " except (ValueError, TypeError) as e: raise _typeerr(\"return\", res, ret) from e\n", + " elif not isinstance(res, ret): raise _typeerr(\"return\", res, ret)\n", + " return res\n", + " return functools.update_wrapper(_f, f)\n", + " if _func is None: return decorator # Decorator was called with arguments\n", + " else: return decorator(_func) # Decorator was called without arguments" ] }, { @@ -6338,12 +6360,32 @@ "outputs": [], "source": [ "@typed\n", - "def discount(price:int, pct:float): \n", + "def discount(price:int, pct:float) -> float:\n", " return (1-pct) * price\n", "\n", "with ExceptionExpected(TypeError): discount(100.0, .1)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can have automatic casting based on heuristics by specifying `typed(cast=True)`. If casting is not possible, a `TypeError` is raised." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "@typed(cast=True)\n", + "def discount(price:int, pct:float) -> float:\n", + " return (1-pct) * price\n", + "\n", + "assert 90.0 == discount(100.0, .1)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -6374,6 +6416,10 @@ "test_eq(foo(1, '2'), 1)\n", "\n", "with ExceptionExpected(TypeError): foo(1,2)\n", + " \n", + "@typed(cast=True)\n", + "def foo(a:int, b:str='a'): return a\n", + "test_eq(foo(1, 2), 1)\n", "\n", "@typed\n", "def foo()->str: return 1\n", @@ -6400,11 +6446,11 @@ "class Foo:\n", " @typed\n", " def __init__(self, a:int, b: int, c:str): pass\n", - " @typed\n", + " @typed(cast=True)\n", " def test(cls, d:str): return d\n", "\n", "with ExceptionExpected(TypeError): Foo(1, 2, 3) \n", - "with ExceptionExpected(TypeError): Foo(1,2, 'a string').test(10)" + "assert isinstance(Foo(1,2, 'a string').test(10), str)" ] }, { @@ -6699,7 +6745,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -6720,21 +6766,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.8" } }, "nbformat": 4, From 136abf500b4320985ee0ee8fa45a958eddc5c1b4 Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Tue, 8 Oct 2024 15:03:38 -0500 Subject: [PATCH 2/5] Add better example show casing new auto casting for typed decorator --- nbs/01_basics.ipynb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index bc08bbd4..79a667c6 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -6383,7 +6383,8 @@ "def discount(price:int, pct:float) -> float:\n", " return (1-pct) * price\n", "\n", - "assert 90.0 == discount(100.0, .1)" + "assert 90.0 == discount(100.5, .1) # will auto cast 100.5 to the int 100\n", + "with ExceptionExpected(TypeError): discount(\"a\", .1)" ] }, { From 5b92f52331f7318741cd26a6efd05d056662a11c Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Tue, 8 Oct 2024 18:41:49 -0500 Subject: [PATCH 3/5] Add new type conversion functions to basics.py --- fastcore/_modidx.py | 9 ++ fastcore/basics.py | 80 ++++++++--- nbs/01_basics.ipynb | 313 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 341 insertions(+), 61 deletions(-) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 13724d59..e8d8f930 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -194,8 +194,17 @@ 'fastcore.basics.stop': ('basics.html#stop', 'fastcore/basics.py'), 'fastcore.basics.store_attr': ('basics.html#store_attr', 'fastcore/basics.py'), 'fastcore.basics.str2bool': ('basics.html#str2bool', 'fastcore/basics.py'), + 'fastcore.basics.str2date': ('basics.html#str2date', 'fastcore/basics.py'), + 'fastcore.basics.str2float': ('basics.html#str2float', 'fastcore/basics.py'), + 'fastcore.basics.str2int': ('basics.html#str2int', 'fastcore/basics.py'), + 'fastcore.basics.str2list': ('basics.html#str2list', 'fastcore/basics.py'), 'fastcore.basics.str_enum': ('basics.html#str_enum', 'fastcore/basics.py'), 'fastcore.basics.strcat': ('basics.html#strcat', 'fastcore/basics.py'), + 'fastcore.basics.to_bool': ('basics.html#to_bool', 'fastcore/basics.py'), + 'fastcore.basics.to_date': ('basics.html#to_date', 'fastcore/basics.py'), + 'fastcore.basics.to_float': ('basics.html#to_float', 'fastcore/basics.py'), + 'fastcore.basics.to_int': ('basics.html#to_int', 'fastcore/basics.py'), + 'fastcore.basics.to_list': ('basics.html#to_list', 'fastcore/basics.py'), 'fastcore.basics.tonull': ('basics.html#tonull', 'fastcore/basics.py'), 'fastcore.basics.true': ('basics.html#true', 'fastcore/basics.py'), 'fastcore.basics.try_attrs': ('basics.html#try_attrs', 'fastcore/basics.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index 79b83178..f996064d 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -18,14 +18,15 @@ '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', 'typed', 'exec_new', 'exec_import', 'str2bool', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', + '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 * -import builtins,types,typing -import pprint +import ast,builtins,pprint,types,typing from copy import copy +from datetime import date try: from types import UnionType except ImportError: UnionType = None @@ -1134,7 +1135,54 @@ def add_props(f, g=None, n=2): def _typeerr(arg, val, typ): return TypeError(f"{arg}=={val} not {typ}") # %% ../nbs/01_basics.ipynb -type_map = {int: int, float: float, str: str, bool: bool} +def str2bool(s): + "Case-insensitive convert string `s` too a bool (`y`,`yes`,`t`,`true`,`on`,`1`->`True`)" + if not isinstance(s,str): return bool(s) + if not s: return False + s = s.lower() + if s in ('y', 'yes', 't', 'true', 'on', '1'): return True + elif s in ('n', 'no', 'f', 'false', 'off', '0'): return False + else: raise _typeerr('s', s, 'bool') + +# %% ../nbs/01_basics.ipynb +def str2int(s) -> int: + "Convert `s` to an `int`" + s = s.lower().strip() + if s in ('', 'none'): return 0 + if s == 'on': return 1 + if s == 'off': return 0 + return int(s) + +# %% ../nbs/01_basics.ipynb +def str2float(s:str): + "Convert `s` to a float" + s = s.lower().strip() + if not s: return 0.0 + return float(s) + +# %% ../nbs/01_basics.ipynb +def str2list(s:str): + "Convert `s` to a list" + s = s.strip() + if not s: return [] + if s[0] != '[': s = '['+s + ']' + return ast.literal_eval(s) + +# %% ../nbs/01_basics.ipynb +def str2date(s:str)->date: + "`date.fromisoformat` with empty string handling" + return date.fromisoformat(s) if s else None + +# %% ../nbs/01_basics.ipynb +def to_bool(arg): return str2bool(arg) if isinstance(arg, str) else bool(arg) +def to_int(arg): return str2int(arg) if isinstance(arg, str) else int(arg) +def to_float(arg): return str2float(arg) if isinstance(arg, str) else float(arg) +def to_list(arg): return str2list(arg) if isinstance(arg,str) else listify(arg) +def to_date(arg): + if isinstance(arg, str): return str2date(arg) + raise _typeerr('arg', arg, 'date') + +type_map = {int: to_int, float: to_float, str: str, bool: to_bool, date: to_date} # %% ../nbs/01_basics.ipynb def typed(_func=None, *, cast=False): @@ -1150,16 +1198,18 @@ def _f(*args, **kwargs): for k,v in kw.items(): if k in anno: expected_type = anno[k] - if cast: - try: kw[k] = type_map[expected_type](v) + if isinstance(v, expected_type): continue + elif cast: + try: kw[k] = type_map.get(expected_type, expected_type)(v) except (ValueError, TypeError) as e: raise _typeerr(k, v, expected_type) from e - elif not isinstance(v, expected_type): raise _typeerr(k, v, expected_type) + else: raise _typeerr(k, v, expected_type) res = f(**kw) if ret is not None: - if cast: - try: res = type_map[ret](res) + if isinstance(res, ret): return res + elif cast: + try: res = type_map.get(ret, ret)(res) except (ValueError, TypeError) as e: raise _typeerr("return", res, ret) from e - elif not isinstance(res, ret): raise _typeerr("return", res, ret) + else: raise _typeerr("return", res, ret) return res return functools.update_wrapper(_f, f) if _func is None: return decorator # Decorator was called with arguments @@ -1178,13 +1228,3 @@ def exec_import(mod, sym): "Import `sym` from `mod` in a new environment" # pref = '' if __name__=='__main__' or mod[0]=='.' else '.' return exec_new(f'from {mod} import {sym}') - -# %% ../nbs/01_basics.ipynb -def str2bool(s): - "Case-insensitive convert string `s` too a bool (`y`,`yes`,`t`,`true`,`on`,`1`->`True`)" - if not isinstance(s,str): return bool(s) - if not s: return False - s = s.lower() - if s in ('y', 'yes', 't', 'true', 'on', '1'): return True - elif s in ('n', 'no', 'f', 'false', 'off', '0'): return False - else: raise ValueError() diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 79a667c6..595869a0 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -17,9 +17,9 @@ "source": [ "#|export\n", "from fastcore.imports import *\n", - "import builtins,types,typing\n", - "import pprint\n", + "import ast,builtins,pprint,types,typing\n", "from copy import copy\n", + "from datetime import date\n", "try: from types import UnionType\n", "except ImportError: UnionType = None" ] @@ -6305,7 +6305,174 @@ "outputs": [], "source": [ "#|export\n", - "type_map = {int: int, float: float, str: str, bool: bool}" + "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", + " if not s: return False\n", + " s = s.lower()\n", + " if s in ('y', 'yes', 't', 'true', 'on', '1'): return True\n", + " elif s in ('n', 'no', 'f', 'false', 'off', '0'): return False\n", + " else: raise _typeerr('s', s, 'bool')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values are 'n', 'no', 'f', 'false', 'off', and '0'. Raises `ValueError` if 'val' is anything else." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for o in \"y YES t True on 1\".split(): assert str2bool(o)\n", + "for o in \"n no FALSE off 0\".split(): assert not str2bool(o)\n", + "for o in 0,None,'',False: assert not str2bool(o)\n", + "for o in 1,True: assert str2bool(o)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def str2int(s) -> int:\n", + " \"Convert `s` to an `int`\"\n", + " s = s.lower().strip()\n", + " if s in ('', 'none'): return 0\n", + " if s == 'on': return 1\n", + " if s == 'off': return 0\n", + " return int(s)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# Test cases for str2int function\n", + "test_eq(str2int('5'), 5)\n", + "test_eq(str2int(' 42 '), 42)\n", + "test_eq(str2int('0'), 0)\n", + "test_eq(str2int(''), 0)\n", + "test_eq(str2int('None'), 0)\n", + "test_eq(str2int('on'), 1)\n", + "test_eq(str2int('off'), 0)\n", + "test_fail(lambda: str2int('not a number'), msg='not an int')\n", + "test_fail(lambda: str2int(42), msg='Already an int') # Non-string input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def str2float(s:str):\n", + " \"Convert `s` to a float\"\n", + " s = s.lower().strip()\n", + " if not s: return 0.0\n", + " return float(s)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# Test cases\n", + "test_eq(str2float('3.14'), 3.14)\n", + "test_eq(str2float(' -2.5 '), -2.5)\n", + "test_eq(str2float('0'), 0.0)\n", + "test_eq(str2float(''), 0.0)\n", + "test_fail(lambda: str2float('not a number'), msg='not a float')\n", + "test_fail(lambda: str2float(3.14), msg='Already a float') # Non-string input" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def str2list(s:str):\n", + " \"Convert `s` to a list\"\n", + " s = s.strip()\n", + " if not s: return []\n", + " if s[0] != '[': s = '['+s + ']'\n", + " return ast.literal_eval(s)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "test_eq(str2list(''), [])\n", + "test_eq(str2list('[1, 2, 3]'), [1, 2, 3])\n", + "test_eq(str2list('[\"a\", \"b\", \"c\"]'), ['a', 'b', 'c'])\n", + "test_eq(str2list(\"1, 2, 3\"), [1, 2, 3])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def str2date(s:str)->date:\n", + " \"`date.fromisoformat` with empty string handling\"\n", + " return date.fromisoformat(s) if s else None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# Test cases for str2date function\n", + "test_eq(str2date('2023-10-08'), date(2023, 10, 8))\n", + "test_eq(str2date('1990-01-01'), date(1990, 1, 1))\n", + "test_eq(str2date(''), None)\n", + "test_fail(lambda: str2date('2023-13-01'), msg='Invalid date')\n", + "test_fail(lambda: str2date('not a date'), msg='Invalid format')\n", + "test_eq(str2date('2023-02-28'), date(2023, 2, 28))\n", + "test_eq(str2date('2024-02-29'), date(2024, 2, 29)) # Leap year\n", + "test_fail(lambda: str2date('2023-02-29'), msg='Invalid date for non-leap year')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#|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", + "def to_list(arg): return str2list(arg) if isinstance(arg,str) else listify(arg)\n", + "def to_date(arg):\n", + " if isinstance(arg, str): return str2date(arg)\n", + " raise _typeerr('arg', arg, 'date')\n", + "\n", + "type_map = {int: to_int, float: to_float, str: str, bool: to_bool, date: to_date}" ] }, { @@ -6328,16 +6495,18 @@ " for k,v in kw.items():\n", " if k in anno:\n", " expected_type = anno[k]\n", - " if cast:\n", - " try: kw[k] = type_map[expected_type](v)\n", + " if isinstance(v, expected_type): continue\n", + " elif cast:\n", + " try: kw[k] = type_map.get(expected_type, expected_type)(v)\n", " except (ValueError, TypeError) as e: raise _typeerr(k, v, expected_type) from e\n", - " elif not isinstance(v, expected_type): raise _typeerr(k, v, expected_type)\n", + " else: raise _typeerr(k, v, expected_type)\n", " res = f(**kw)\n", " if ret is not None:\n", - " if cast:\n", - " try: res = type_map[ret](res)\n", + " if isinstance(res, ret): return res\n", + " elif cast:\n", + " try: res = type_map.get(ret, ret)(res)\n", " except (ValueError, TypeError) as e: raise _typeerr(\"return\", res, ret) from e\n", - " elif not isinstance(res, ret): raise _typeerr(\"return\", res, ret)\n", + " else: raise _typeerr(\"return\", res, ret)\n", " return res\n", " return functools.update_wrapper(_f, f)\n", " if _func is None: return decorator # Decorator was called with arguments\n", @@ -6383,7 +6552,8 @@ "def discount(price:int, pct:float) -> float:\n", " return (1-pct) * price\n", "\n", - "assert 90.0 == discount(100.5, .1) # will auto cast 100.5 to the int 100\n", + "assert 90.0 == discount(100.5, .1) # will auto cast 100.5 to the int 100\n", + "assert 90.0 == discount(' 100 ', .1) # will auto cast the str \"100\" to the int 100\n", "with ExceptionExpected(TypeError): discount(\"a\", .1)" ] }, @@ -6400,12 +6570,71 @@ "metadata": {}, "outputs": [], "source": [ + "@typed\n", "def discount(price:int|float, pct:float): \n", " return (1-pct) * price\n", "\n", "assert 90.0 == discount(100.0, .1)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "price==100.0 not typing.Optional[int]", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[375], line 17\u001b[0m, in \u001b[0;36mtyped..decorator.._f\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m cast:\n\u001b[0;32m---> 17\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m: kw[k] \u001b[38;5;241m=\u001b[39m \u001b[43mtype_map\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[43mexpected_type\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexpected_type\u001b[49m\u001b[43m)\u001b[49m\u001b[43m(\u001b[49m\u001b[43mv\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mValueError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e: \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n", + "File \u001b[0;32m~/.local/share/uv/python/cpython-3.10.15-macos-aarch64-none/lib/python3.10/typing.py:957\u001b[0m, in \u001b[0;36m_BaseGenericAlias.__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 955\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mType \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m cannot be instantiated; \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 956\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124muse \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m__origin__\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m() instead\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 957\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__origin__\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 958\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n", + "File \u001b[0;32m~/.local/share/uv/python/cpython-3.10.15-macos-aarch64-none/lib/python3.10/typing.py:387\u001b[0m, in \u001b[0;36m_SpecialForm.__call__\u001b[0;34m(self, *args, **kwds)\u001b[0m\n\u001b[1;32m 386\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__call__\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwds):\n\u001b[0;32m--> 387\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot instantiate \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mTypeError\u001b[0m: Cannot instantiate typing.Union", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[379], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;129m@typed\u001b[39m(cast\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdiscount\u001b[39m(price:\u001b[38;5;28mint\u001b[39m\u001b[38;5;241m|\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, pct:\u001b[38;5;28mfloat\u001b[39m): \u001b[38;5;66;03m# need to handle and there should be some code to do this already\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m (\u001b[38;5;241m1\u001b[39m\u001b[38;5;241m-\u001b[39mpct) \u001b[38;5;241m*\u001b[39m price\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;241m90.0\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[43mdiscount\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m100.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m.1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[375], line 18\u001b[0m, in \u001b[0;36mtyped..decorator.._f\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m cast:\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m: kw[k] \u001b[38;5;241m=\u001b[39m type_map\u001b[38;5;241m.\u001b[39mget(expected_type, expected_type)(v)\n\u001b[0;32m---> 18\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mValueError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e: \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m: \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type)\n\u001b[1;32m 20\u001b[0m res \u001b[38;5;241m=\u001b[39m f(\u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkw)\n", + "\u001b[0;31mTypeError\u001b[0m: price==100.0 not typing.Optional[int]" + ] + } + ], + "source": [ + "@typed(cast=True)\n", + "def discount(price:int|None, pct:float): # need to handle and there should be some code to do this already\n", + " return (1-pct) * price\n", + "\n", + "assert 90.0 == discount(100.0, .1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "ename": "KeyError", + "evalue": "typing.Union[int, float]", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[361], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;129m@typed\u001b[39m(cast\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdiscount\u001b[39m(price:\u001b[38;5;28mint\u001b[39m\u001b[38;5;241m|\u001b[39m\u001b[38;5;28mfloat\u001b[39m, pct:\u001b[38;5;28mfloat\u001b[39m): \u001b[38;5;66;03m# don't need to handle\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m (\u001b[38;5;241m1\u001b[39m\u001b[38;5;241m-\u001b[39mpct) \u001b[38;5;241m*\u001b[39m price\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;241m90.0\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[43mdiscount\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m100.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m.1\u001b[39;49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[347], line 16\u001b[0m, in \u001b[0;36mtyped..decorator.._f\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 14\u001b[0m expected_type \u001b[38;5;241m=\u001b[39m anno[k]\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m cast:\n\u001b[0;32m---> 16\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m: kw[k] \u001b[38;5;241m=\u001b[39m \u001b[43mtype_map\u001b[49m\u001b[43m[\u001b[49m\u001b[43mexpected_type\u001b[49m\u001b[43m]\u001b[49m(v)\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mValueError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e: \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(v, expected_type): \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type)\n", + "\u001b[0;31mKeyError\u001b[0m: typing.Union[int, float]" + ] + } + ], + "source": [ + "@typed(cast=True)\n", + "def discount(price:int|float, pct:float): # don't need to handle\n", + " return (1-pct) * price\n", + "\n", + "assert 90.0 == discount(100.0, .1)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -6455,18 +6684,10 @@ ] }, { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "#|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", - " g = {'__name__': __name__, '__package__': pkg}\n", - " exec(code, g)\n", - " return g" + "It also works with custom types." ] }, { @@ -6475,8 +6696,11 @@ "metadata": {}, "outputs": [], "source": [ - "g = exec_new('a=1')\n", - "test_eq(g['a'], 1)" + "@typed\n", + "def test_foo(foo: Foo): pass\n", + "\n", + "with ExceptionExpected(TypeError): test_foo(1)\n", + "test_foo(Foo(1, 2, 'a string'))" ] }, { @@ -6485,11 +6709,16 @@ "metadata": {}, "outputs": [], "source": [ - "#|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", - " return exec_new(f'from {mod} import {sym}')" + "class Bar:\n", + " @typed\n", + " def __init__(self, a:int): self.a = a\n", + "@typed(cast=True)\n", + "def test_bar(foo: Bar): return foo\n", + "\n", + "assert isinstance(test_bar(1), Bar)\n", + "test_eq(test_bar(1).a, 1)\n", + "\n", + "with ExceptionExpected(TypeError): test_bar(\"test\")" ] }, { @@ -6499,21 +6728,22 @@ "outputs": [], "source": [ "#|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", - " if not s: return False\n", - " s = s.lower()\n", - " if s in ('y', 'yes', 't', 'true', 'on', '1'): return True\n", - " elif s in ('n', 'no', 'f', 'false', 'off', '0'): return False\n", - " else: raise ValueError()" + "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", + " g = {'__name__': __name__, '__package__': pkg}\n", + " exec(code, g)\n", + " return g" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values are 'n', 'no', 'f', 'false', 'off', and '0'. Raises `ValueError` if 'val' is anything else." + "g = exec_new('a=1')\n", + "test_eq(g['a'], 1)" ] }, { @@ -6522,10 +6752,11 @@ "metadata": {}, "outputs": [], "source": [ - "for o in \"y YES t True on 1\".split(): assert str2bool(o)\n", - "for o in \"n no FALSE off 0\".split(): assert not str2bool(o)\n", - "for o in 0,None,'',False: assert not str2bool(o)\n", - "for o in 1,True: assert str2bool(o)" + "#|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", + " return exec_new(f'from {mod} import {sym}')" ] }, { From f96b73a7b5420144ee3c16fb3e639eafe611b94b Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Tue, 8 Oct 2024 21:17:03 -0500 Subject: [PATCH 4/5] Update the function to handle optional types and to raise an error when casting with union types --- fastcore/basics.py | 10 ++++--- nbs/01_basics.ipynb | 64 +++++++++++++++------------------------------ 2 files changed, 28 insertions(+), 46 deletions(-) diff --git a/fastcore/basics.py b/fastcore/basics.py index f996064d..829c064e 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -1197,12 +1197,16 @@ def _f(*args, **kwargs): for i,arg in enumerate(args): kw[names[i]] = arg for k,v in kw.items(): if k in anno: - expected_type = anno[k] - if isinstance(v, expected_type): continue + expected_types = tuplify(union2tuple(anno[k])) + if isinstance(v, expected_types): continue elif cast: + expected_types = listify(filter(lambda x: x is not NoneType, expected_types)) + assert not len(expected_types) > 1, "Cannot cast with union types." + # Grab the first type that is not None + expected_type = expected_types[0] try: kw[k] = type_map.get(expected_type, expected_type)(v) except (ValueError, TypeError) as e: raise _typeerr(k, v, expected_type) from e - else: raise _typeerr(k, v, expected_type) + else: raise _typeerr(k, v, expected_types) res = f(**kw) if ret is not None: if isinstance(res, ret): return res diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 595869a0..702c9ded 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -6494,12 +6494,16 @@ " for i,arg in enumerate(args): kw[names[i]] = arg\n", " for k,v in kw.items():\n", " if k in anno:\n", - " expected_type = anno[k]\n", - " if isinstance(v, expected_type): continue\n", + " expected_types = tuplify(union2tuple(anno[k]))\n", + " if isinstance(v, expected_types): continue\n", " elif cast:\n", + " expected_types = listify(filter(lambda x: x is not NoneType, expected_types))\n", + " assert not len(expected_types) > 1, \"Cannot cast with union types.\"\n", + " # Grab the first type that is not None\n", + " expected_type = expected_types[0]\n", " try: kw[k] = type_map.get(expected_type, expected_type)(v)\n", " except (ValueError, TypeError) as e: raise _typeerr(k, v, expected_type) from e\n", - " else: raise _typeerr(k, v, expected_type)\n", + " else: raise _typeerr(k, v, expected_types)\n", " res = f(**kw)\n", " if ret is not None:\n", " if isinstance(res, ret): return res\n", @@ -6581,58 +6585,33 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "TypeError", - "evalue": "price==100.0 not typing.Optional[int]", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[375], line 17\u001b[0m, in \u001b[0;36mtyped..decorator.._f\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m cast:\n\u001b[0;32m---> 17\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m: kw[k] \u001b[38;5;241m=\u001b[39m \u001b[43mtype_map\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[43mexpected_type\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexpected_type\u001b[49m\u001b[43m)\u001b[49m\u001b[43m(\u001b[49m\u001b[43mv\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mValueError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e: \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n", - "File \u001b[0;32m~/.local/share/uv/python/cpython-3.10.15-macos-aarch64-none/lib/python3.10/typing.py:957\u001b[0m, in \u001b[0;36m_BaseGenericAlias.__call__\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 955\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mType \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m cannot be instantiated; \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 956\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124muse \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m__origin__\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m() instead\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m--> 957\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m__origin__\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 958\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n", - "File \u001b[0;32m~/.local/share/uv/python/cpython-3.10.15-macos-aarch64-none/lib/python3.10/typing.py:387\u001b[0m, in \u001b[0;36m_SpecialForm.__call__\u001b[0;34m(self, *args, **kwds)\u001b[0m\n\u001b[1;32m 386\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__call__\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwds):\n\u001b[0;32m--> 387\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mCannot instantiate \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n", - "\u001b[0;31mTypeError\u001b[0m: Cannot instantiate typing.Union", - "\nThe above exception was the direct cause of the following exception:\n", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[379], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;129m@typed\u001b[39m(cast\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdiscount\u001b[39m(price:\u001b[38;5;28mint\u001b[39m\u001b[38;5;241m|\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m, pct:\u001b[38;5;28mfloat\u001b[39m): \u001b[38;5;66;03m# need to handle and there should be some code to do this already\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m (\u001b[38;5;241m1\u001b[39m\u001b[38;5;241m-\u001b[39mpct) \u001b[38;5;241m*\u001b[39m price\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;241m90.0\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[43mdiscount\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m100.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m.1\u001b[39;49m\u001b[43m)\u001b[49m\n", - "Cell \u001b[0;32mIn[375], line 18\u001b[0m, in \u001b[0;36mtyped..decorator.._f\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m cast:\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m: kw[k] \u001b[38;5;241m=\u001b[39m type_map\u001b[38;5;241m.\u001b[39mget(expected_type, expected_type)(v)\n\u001b[0;32m---> 18\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mValueError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e: \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n\u001b[1;32m 19\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m: \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type)\n\u001b[1;32m 20\u001b[0m res \u001b[38;5;241m=\u001b[39m f(\u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkw)\n", - "\u001b[0;31mTypeError\u001b[0m: price==100.0 not typing.Optional[int]" - ] - } - ], + "outputs": [], "source": [ "@typed(cast=True)\n", - "def discount(price:int|None, pct:float): # need to handle and there should be some code to do this already\n", + "def discount(price:int|None, pct:float):\n", " return (1-pct) * price\n", "\n", "assert 90.0 == discount(100.0, .1)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We currently do not support union types when casting." + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "ename": "KeyError", - "evalue": "typing.Union[int, float]", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[361], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;129m@typed\u001b[39m(cast\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdiscount\u001b[39m(price:\u001b[38;5;28mint\u001b[39m\u001b[38;5;241m|\u001b[39m\u001b[38;5;28mfloat\u001b[39m, pct:\u001b[38;5;28mfloat\u001b[39m): \u001b[38;5;66;03m# don't need to handle\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m (\u001b[38;5;241m1\u001b[39m\u001b[38;5;241m-\u001b[39mpct) \u001b[38;5;241m*\u001b[39m price\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01massert\u001b[39;00m \u001b[38;5;241m90.0\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[43mdiscount\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m100.0\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m.1\u001b[39;49m\u001b[43m)\u001b[49m\n", - "Cell \u001b[0;32mIn[347], line 16\u001b[0m, in \u001b[0;36mtyped..decorator.._f\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 14\u001b[0m expected_type \u001b[38;5;241m=\u001b[39m anno[k]\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m cast:\n\u001b[0;32m---> 16\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m: kw[k] \u001b[38;5;241m=\u001b[39m \u001b[43mtype_map\u001b[49m\u001b[43m[\u001b[49m\u001b[43mexpected_type\u001b[49m\u001b[43m]\u001b[49m(v)\n\u001b[1;32m 17\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m (\u001b[38;5;167;01mValueError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m e: \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type) \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01me\u001b[39;00m\n\u001b[1;32m 18\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(v, expected_type): \u001b[38;5;28;01mraise\u001b[39;00m _typeerr(k, v, expected_type)\n", - "\u001b[0;31mKeyError\u001b[0m: typing.Union[int, float]" - ] - } - ], + "outputs": [], "source": [ "@typed(cast=True)\n", - "def discount(price:int|float, pct:float): # don't need to handle\n", + "def discount(price:int|float, pct:float):\n", " return (1-pct) * price\n", "\n", - "assert 90.0 == discount(100.0, .1)" + "with ExceptionExpected(AssertionError): assert 90.0 == discount(\"100.0\", .1)" ] }, { @@ -6713,12 +6692,11 @@ " @typed\n", " def __init__(self, a:int): self.a = a\n", "@typed(cast=True)\n", - "def test_bar(foo: Bar): return foo\n", + "def test_bar(bar: Bar): return bar\n", "\n", "assert isinstance(test_bar(1), Bar)\n", "test_eq(test_bar(1).a, 1)\n", - "\n", - "with ExceptionExpected(TypeError): test_bar(\"test\")" + "with ExceptionExpected(TypeError): test_bar(\"foobar\")" ] }, { From 9474b9b1add0ea9ff3bb853f996aa4afeebbcc92 Mon Sep 17 00:00:00 2001 From: ncoop57 Date: Tue, 8 Oct 2024 22:57:48 -0500 Subject: [PATCH 5/5] Update docs by hiding some additional tests that don't flow well in the docs --- nbs/01_basics.ipynb | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 8a537ea7..87be81a5 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -6632,15 +6632,8 @@ "def discount(price:int|float, pct:float): \n", " return (1-pct) * price\n", "\n", - "assert 90.0 == discount(100.0, .1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ + "assert 90.0 == discount(100.0, .1)\n", + "\n", "@typed(cast=True)\n", "def discount(price:int|None, pct:float):\n", " return (1-pct) * price\n", @@ -6674,6 +6667,7 @@ "metadata": {}, "outputs": [], "source": [ + "#| hide\n", "@typed\n", "def foo(a:int, b:str='a'): return a\n", "test_eq(foo(1, '2'), 1)\n",