diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 99e4634f..25abf51c 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -195,8 +195,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 5f1db4f9..16d36336 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -4,28 +4,29 @@ # %% auto 0 __all__ = ['defaults', 'null', 'num_methods', 'rnum_methods', 'inum_methods', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4', 'Self', - 'ifnone', 'maybe_attr', 'basic_repr', 'BasicRepr', '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', - 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle', 'zip_cycle', 'sorted_ex', 'not_', - 'argwhere', 'filter_ex', 'renumerate', 'first', 'only', 'nested_attr', 'nested_setdefault', + 'type_map', 'ifnone', 'maybe_attr', 'basic_repr', 'BasicRepr', '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', 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle', 'zip_cycle', 'sorted_ex', + 'not_', 'argwhere', 'filter_ex', 'renumerate', 'first', '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', '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 @@ -1141,21 +1142,89 @@ 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) +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): + "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_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_types) + res = f(**kw) + if ret is not None: + 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 + 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 + else: return decorator(_func) # Decorator was called without arguments # %% ../nbs/01_basics.ipynb def exec_new(code): @@ -1170,13 +1239,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 0b3b4b58..87be81a5 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" ] @@ -6359,21 +6359,216 @@ "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)" + "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}" + ] + }, + { + "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_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_types)\n", + " res = f(**kw)\n", + " if ret is not None:\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", + " 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", + " else: return decorator(_func) # Decorator was called without arguments" ] }, { @@ -6392,12 +6587,34 @@ "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.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)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -6411,23 +6628,55 @@ "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)\n", + "\n", + "@typed(cast=True)\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": [], "source": [ + "@typed(cast=True)\n", + "def discount(price:int|float, pct:float):\n", + " return (1-pct) * price\n", + "\n", + "with ExceptionExpected(AssertionError): assert 90.0 == discount(\"100.0\", .1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", "@typed\n", "def foo(a:int, b:str='a'): return a\n", "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", @@ -6454,26 +6703,18 @@ "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)" ] }, { - "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." ] }, { @@ -6482,8 +6723,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'))" ] }, { @@ -6492,11 +6736,15 @@ "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(bar: Bar): return bar\n", + "\n", + "assert isinstance(test_bar(1), Bar)\n", + "test_eq(test_bar(1).a, 1)\n", + "with ExceptionExpected(TypeError): test_bar(\"foobar\")" ] }, { @@ -6506,21 +6754,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)" ] }, { @@ -6529,10 +6778,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}')" ] }, {