diff --git a/fastcore/_nbdev.py b/fastcore/_nbdev.py index f9db615a..4dcc4509 100644 --- a/fastcore/_nbdev.py +++ b/fastcore/_nbdev.py @@ -44,6 +44,10 @@ "otherwise": "01_basics.ipynb", "custom_dir": "01_basics.ipynb", "AttrDict": "01_basics.ipynb", + "type_hints": "01_basics.ipynb", + "annotations": "01_basics.ipynb", + "anno_ret": "01_basics.ipynb", + "argnames": "01_basics.ipynb", "with_cast": "01_basics.ipynb", "store_attr": "01_basics.ipynb", "attrdict": "01_basics.ipynb", @@ -165,8 +169,6 @@ "run_procs": "03_xtras.ipynb", "parallel_gen": "03_xtras.ipynb", "threaded": "03_xtras.ipynb", - "type_hints": "04_dispatch.ipynb", - "anno_ret": "04_dispatch.ipynb", "lenient_issubclass": "04_dispatch.ipynb", "sorted_topologically": "04_dispatch.ipynb", "TypeDispatch": "04_dispatch.ipynb", diff --git a/fastcore/basics.py b/fastcore/basics.py index d98b309d..7ef5adc2 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -3,17 +3,18 @@ __all__ = ['defaults', 'ifnone', 'maybe_attr', 'basic_repr', 'is_array', 'listify', 'true', 'NullType', 'null', 'tonull', 'get_class', 'mk_class', 'wrap_class', 'ignore_exceptions', 'exec_local', 'risinstance', 'Inf', 'in_', 'lt', 'gt', 'le', 'ge', 'eq', 'ne', 'add', 'sub', 'mul', 'truediv', 'is_', 'is_not', 'in_', 'true', - 'stop', 'gen', 'chunked', 'otherwise', 'custom_dir', 'AttrDict', 'with_cast', 'store_attr', 'attrdict', - 'properties', 'camel2snake', 'snake2camel', 'class2attr', 'getattrs', 'hasattrs', 'setattrs', 'try_attrs', - 'ShowPrint', 'Int', 'Str', 'Float', 'detuplify', 'replicate', 'setify', 'merge', 'range_of', 'groupby', - 'last_index', 'filter_dict', 'filter_keys', 'filter_values', 'cycle', 'zip_cycle', 'sorted_ex', - 'negate_func', 'argwhere', 'filter_ex', 'range_of', 'renumerate', 'first', 'nested_attr', 'nested_idx', - 'num_methods', 'rnum_methods', 'inum_methods', 'fastuple', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4', 'bind', - 'map_ex', 'compose', 'maps', 'partialler', 'instantiate', 'using_attr', 'Self', 'Self', 'PrettyString', - 'even_mults', 'num_cpus', 'add_props', 'typed'] + 'stop', 'gen', 'chunked', 'otherwise', 'custom_dir', 'AttrDict', 'type_hints', 'annotations', 'anno_ret', + 'argnames', 'with_cast', 'store_attr', 'attrdict', 'properties', 'camel2snake', 'snake2camel', 'class2attr', + 'getattrs', 'hasattrs', 'setattrs', 'try_attrs', 'ShowPrint', 'Int', 'Str', 'Float', 'detuplify', + 'replicate', 'setify', 'merge', 'range_of', 'groupby', 'last_index', 'filter_dict', 'filter_keys', + 'filter_values', 'cycle', 'zip_cycle', 'sorted_ex', 'negate_func', 'argwhere', 'filter_ex', 'range_of', + 'renumerate', 'first', 'nested_attr', 'nested_idx', 'num_methods', 'rnum_methods', 'inum_methods', + 'fastuple', 'arg0', 'arg1', 'arg2', 'arg3', 'arg4', 'bind', 'map_ex', 'compose', 'maps', 'partialler', + 'instantiate', 'using_attr', 'Self', 'Self', 'PrettyString', 'even_mults', 'num_cpus', 'add_props', 'typed'] # Cell from .imports import * +import weakref # Cell defaults = SimpleNamespace() @@ -216,12 +217,37 @@ 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 custom_dir(self, list(self.keys())) +# Cell +def type_hints(f): + "Same as `typing.get_type_hints` but returns `{}` if not allowed type" + return typing.get_type_hints(f) if isinstance(f, typing._allowed_types) else {} + +# Cell +def annotations(o): + "Annotations for `o`, or `type(o)`" + res = {} + if not o: return res + res = type_hints(o) + if not res: res = type_hints(getattr(o,'__init__',None)) + if not res: res = type_hints(type(o)) + return res + +# Cell +def anno_ret(func): + "Get the return annotation of `func`" + return annotations(func).get('return', None) if func else None + +# Cell +def argnames(f): + "Names of arguments to function `f`" + code = f.__code__ + return code.co_varnames[:code.co_argcount] + # Cell def with_cast(f): "Decorator which uses any parameter annotations as preprocessing functions" - anno = f.__annotations__ - params = f.__code__.co_varnames[:f.__code__.co_argcount] - defaults = dict(zip(reversed(params), reversed(f.__defaults__))) if f.__defaults__ else {} + anno,params = annotations(f),argnames(f) + defaults = dict(zip(reversed(params), reversed(f.__defaults__ or {}))) @functools.wraps(f) def _inner(*args, **kwargs): args = list(args) @@ -236,10 +262,13 @@ def _inner(*args, **kwargs): # Cell def _store_attr(self, anno, **attrs): + stored = self.__stored_args__ for n,v in attrs.items(): if n in anno: v = anno[n](v) setattr(self, n, v) - self.__stored_args__[n] = v + try: v = weakref.proxy(v) + except TypeError: pass + stored[n] = v # Cell def store_attr(names=None, self=None, but='', cast=False, **attrs): @@ -249,7 +278,7 @@ def store_attr(names=None, self=None, but='', cast=False, **attrs): if self: args = ('self', *args) else: self = fr.f_locals[args[0]] if not hasattr(self, '__stored_args__'): self.__stored_args__ = {} - anno = self.__class__.__init__.__annotations__ if cast else {} + anno = annotations(self) if cast else {} if not attrs: ns = re.split(', *', names) if names else args[1:] attrs = {n:fr.f_locals[n] for n in ns} @@ -258,9 +287,9 @@ def store_attr(names=None, self=None, but='', cast=False, **attrs): return _store_attr(self, anno, **attrs) # Cell -def attrdict(o, *ks): +def attrdict(o, *ks, default=None): "Dict from each `k` in `ks` to `getattr(o,k)`" - return {k:getattr(o,k) for k in ks} + return {k:getattr(o, k, default) for k in ks} # Cell def properties(cls, *ps): @@ -668,7 +697,7 @@ def _typeerr(arg, val, typ): return TypeError(f"{arg}=={val} not {typ}") def typed(f): "Decorator to check param and return types at runtime" names = f.__code__.co_varnames - anno = f.__annotations__ + anno = annotations(f) ret = anno.pop('return',None) def _f(*args,**kwargs): kw = {**kwargs} diff --git a/fastcore/dispatch.py b/fastcore/dispatch.py index 62249c16..02d2ad10 100644 --- a/fastcore/dispatch.py +++ b/fastcore/dispatch.py @@ -1,26 +1,14 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: nbs/04_dispatch.ipynb (unless otherwise specified). -__all__ = ['type_hints', 'anno_ret', 'lenient_issubclass', 'sorted_topologically', 'TypeDispatch', 'DispatchReg', - 'typedispatch', 'cast', 'retain_meta', 'default_set_meta', 'retain_type', 'retain_types', 'explode_types'] +__all__ = ['lenient_issubclass', 'sorted_topologically', 'TypeDispatch', 'DispatchReg', 'typedispatch', 'cast', + 'retain_meta', 'default_set_meta', 'retain_type', 'retain_types', 'explode_types'] # Cell from .imports import * from .foundation import * from .utils import * -from collections import defaultdict - -# Cell -def type_hints(f): - "Same as `typing.get_type_hints` but returns `{}` if not allowed type" - return typing.get_type_hints(f) if isinstance(f, typing._allowed_types) else {} -# Cell -def anno_ret(func): - "Get the return annotation of `func`" - if not func: return None - ann = type_hints(func) - if not ann: return None - return ann.get('return') +from collections import defaultdict # Cell def lenient_issubclass(cls, types): @@ -114,7 +102,7 @@ def returns_none(self, 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__", v.__class__.__name__)}' + 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) diff --git a/fastcore/imports.py b/fastcore/imports.py index f0cf75ba..f0b236f8 100644 --- a/fastcore/imports.py +++ b/fastcore/imports.py @@ -1,4 +1,4 @@ -import sys,os,re,shutil,typing,itertools,operator,functools,math,warnings,functools,io +import sys,os,re,typing,itertools,operator,functools,math,warnings,functools,io from operator import itemgetter,attrgetter from warnings import warn diff --git a/fastcore/nb_imports.py b/fastcore/nb_imports.py index 9deade09..ff8131ed 100644 --- a/fastcore/nb_imports.py +++ b/fastcore/nb_imports.py @@ -1,6 +1,6 @@ import numpy as np import matplotlib.pyplot as plt -import numbers,tempfile,pickle,random,inspect +import numbers,tempfile,pickle,random,inspect,shutil from PIL import Image from numpy import array,ndarray diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index c04da60e..d9dec8ab 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -16,7 +16,8 @@ "outputs": [], "source": [ "#export\n", - "from fastcore.imports import *" + "from fastcore.imports import *\n", + "import weakref" ] }, { @@ -1204,6 +1205,199 @@ "assert 'a' in dir(d)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#exports\n", + "def type_hints(f):\n", + " \"Same as `typing.get_type_hints` but returns `{}` if not allowed type\"\n", + " return typing.get_type_hints(f) if isinstance(f, typing._allowed_types) else {}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Below is a list of allowed types for type hints in python:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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" + } + ], + "source": [ + "list(typing._allowed_types)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For example, type `func` is allowed so `type_hints` returns the same value as `typing.get_hints`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def f(a:int)->bool: ... # a function with type hints (allowed)\n", + "exp = {'a':int,'return':bool}\n", + "test_eq(type_hints(f), typing.get_type_hints(f))\n", + "test_eq(type_hints(f), exp)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "However, `class` is not an allowed type, so `type_hints` returns `{}`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class _T:\n", + " def __init__(self, a:int=0)->bool: ...\n", + "assert not type_hints(_T)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#export\n", + "def annotations(o):\n", + " \"Annotations for `o`, or `type(o)`\"\n", + " res = {}\n", + " if not o: return res\n", + " res = type_hints(o)\n", + " if not res: res = type_hints(getattr(o,'__init__',None))\n", + " if not res: res = type_hints(type(o))\n", + " return res" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This supports a wider range of situations than `type_hints`, by checking `type()` and `__init__` for annotations too:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for o in _T,_T(),_T.__init__,f: test_eq(annotations(o), exp)\n", + "assert not annotations(int)\n", + "assert not annotations(print)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#export\n", + "def anno_ret(func):\n", + " \"Get the return annotation of `func`\"\n", + " return annotations(func).get('return', None) if func else None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def f(x) -> float: return x\n", + "test_eq(anno_ret(f), float)\n", + "\n", + "def f(x) -> typing.Tuple[float,float]: return x\n", + "test_eq(anno_ret(f), typing.Tuple[float,float])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If your return annotation is `None`, `anno_ret` will return `NoneType` (and not `None`):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def f(x) -> None: return x\n", + "\n", + "test_eq(anno_ret(f), NoneType)\n", + "assert anno_ret(f) is not None # returns NoneType instead of None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If your function does not have a return type, or if you pass in `None` instead of a function, then `anno_ret` returns `None`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def f(x): return x\n", + "\n", + "test_eq(anno_ret(f), None)\n", + "test_eq(anno_ret(None), None) # instead of passing in a func, pass in None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#export\n", + "def argnames(f):\n", + " \"Names of arguments to function `f`\"\n", + " code = f.__code__\n", + " return code.co_varnames[:code.co_argcount]" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1213,9 +1407,8 @@ "#export\n", "def with_cast(f):\n", " \"Decorator which uses any parameter annotations as preprocessing functions\"\n", - " anno = f.__annotations__\n", - " params = f.__code__.co_varnames[:f.__code__.co_argcount]\n", - " defaults = dict(zip(reversed(params), reversed(f.__defaults__))) if f.__defaults__ else {}\n", + " anno,params = annotations(f),argnames(f)\n", + " defaults = dict(zip(reversed(params), reversed(f.__defaults__ or {})))\n", " @functools.wraps(f)\n", " def _inner(*args, **kwargs):\n", " args = list(args)\n", @@ -1250,10 +1443,13 @@ "source": [ "#export\n", "def _store_attr(self, anno, **attrs):\n", + " stored = self.__stored_args__\n", " for n,v in attrs.items():\n", " if n in anno: v = anno[n](v)\n", " setattr(self, n, v)\n", - " self.__stored_args__[n] = v" + " try: v = weakref.proxy(v)\n", + " except TypeError: pass\n", + " stored[n] = v" ] }, { @@ -1270,7 +1466,7 @@ " if self: args = ('self', *args)\n", " else: self = fr.f_locals[args[0]]\n", " if not hasattr(self, '__stored_args__'): self.__stored_args__ = {}\n", - " anno = self.__class__.__init__.__annotations__ if cast else {}\n", + " anno = annotations(self) if cast else {}\n", " if not attrs:\n", " ns = re.split(', *', names) if names else args[1:]\n", " attrs = {n:fr.f_locals[n] for n in ns}\n", @@ -1551,9 +1747,9 @@ "outputs": [], "source": [ "#export\n", - "def attrdict(o, *ks):\n", + "def attrdict(o, *ks, default=None):\n", " \"Dict from each `k` in `ks` to `getattr(o,k)`\"\n", - " return {k:getattr(o,k) for k in ks}" + " return {k:getattr(o, k, default) for k in ks}" ] }, { @@ -3536,7 +3732,7 @@ "def typed(f):\n", " \"Decorator to check param and return types at runtime\"\n", " names = f.__code__.co_varnames\n", - " anno = f.__annotations__\n", + " anno = annotations(f)\n", " ret = anno.pop('return',None)\n", " def _f(*args,**kwargs):\n", " kw = {**kwargs}\n", @@ -3798,7 +3994,6 @@ "Converted 03_xtras.ipynb.\n", "Converted 04_dispatch.ipynb.\n", "Converted 05_transform.ipynb.\n", - "Converted 06_logargs.ipynb.\n", "Converted 07_meta.ipynb.\n", "Converted 08_script.ipynb.\n", "Converted index.ipynb.\n" diff --git a/nbs/04_dispatch.ipynb b/nbs/04_dispatch.ipynb index 2d045fbc..d7b8eb3c 100644 --- a/nbs/04_dispatch.ipynb +++ b/nbs/04_dispatch.ipynb @@ -19,6 +19,7 @@ "from fastcore.imports import *\n", "from fastcore.foundation import *\n", "from fastcore.utils import *\n", + "\n", "from collections import defaultdict" ] }, @@ -51,149 +52,6 @@ "## Helpers" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#exports\n", - "def type_hints(f):\n", - " \"Same as `typing.get_type_hints` but returns `{}` if not allowed type\"\n", - " return typing.get_type_hints(f) if isinstance(f, typing._allowed_types) else {}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Below is a list of allowed types for type hints in python:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(#7) ['function','builtin_function_or_method','method','module','wrapper_descriptor','method-wrapper','method_descriptor']" - ] - }, - "execution_count": null, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "L(typing._allowed_types).attrgot(\"__name__\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For example, type `func` is allowed so `type_hints` returns the same value as `typing.get_hints`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f(a:int, b:str): pass # a function with type hints (allowed)\n", - "\n", - "assert type_hints(f) == typing.get_type_hints(f)\n", - "test_eq(type_hints(f), {'a': int, 'b': str})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, `class` is not an allowed type, so `type_hints` returns `{}`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class _T:\n", - " def __init__(self, a:int, b:str): pass\n", - "\n", - "test_eq(type_hints(_T), {})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#export\n", - "def anno_ret(func):\n", - " \"Get the return annotation of `func`\"\n", - " if not func: return None\n", - " ann = type_hints(func)\n", - " if not ann: return None\n", - " return ann.get('return')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f(x) -> float: return x\n", - "test_eq(anno_ret(f), float)\n", - "\n", - "def f(x) -> typing.Tuple[float,float]: return x\n", - "test_eq(anno_ret(f), typing.Tuple[float,float])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If your return annotation is `None`, `anno_ret` will return `NoneType` (and not `None`):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f(x) -> None: return x\n", - "\n", - "test_eq(anno_ret(f), NoneType)\n", - "assert anno_ret(f) is not None # returns NoneType instead of None" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If your function does not have a return type, or if you pass in `None` instead of a function, then `anno_ret` returns `None`:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def f(x): return x\n", - "\n", - "test_eq(anno_ret(f), None)\n", - "test_eq(anno_ret(None), None) # instead of passing in a func, pass in None" - ] - }, { "cell_type": "code", "execution_count": null, @@ -467,7 +325,7 @@ "\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__\", v.__class__.__name__)}'\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",