From e24ac7d3198fecbe9631da6982f2831ea91e7ee0 Mon Sep 17 00:00:00 2001 From: Salehbigdeli Date: Thu, 2 Mar 2023 04:06:48 +0100 Subject: [PATCH 1/2] Bugfix patch classmethod not supporting subclasses correctly --- fastcore/basics.py | 36 ++++---- nbs/01_basics.ipynb | 220 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 208 insertions(+), 48 deletions(-) diff --git a/fastcore/basics.py b/fastcore/basics.py index 4c9443cb..d8760fee 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -934,7 +934,7 @@ def copy_func(f): fn.__qualname__ = f.__qualname__ return fn -# %% ../nbs/01_basics.ipynb 366 +# %% ../nbs/01_basics.ipynb 367 def patch_to(cls, as_prop=False, cls_method=False): "Decorator: add `f` to `cls`" if not isinstance(cls, (tuple,list)): cls=(cls,) @@ -946,14 +946,14 @@ def _inner(f): for o in functools.WRAPPER_ASSIGNMENTS: setattr(nf, o, getattr(f,o)) nf.__qualname__ = f"{c_.__name__}.{nm}" if cls_method: - setattr(c_, nm, MethodType(nf, c_)) + setattr(c_, nm, _clsmethod(nf)) else: setattr(c_, nm, property(nf) if as_prop else nf) # Avoid clobbering existing functions return globals().get(nm, builtins.__dict__.get(nm, None)) return _inner -# %% ../nbs/01_basics.ipynb 377 +# %% ../nbs/01_basics.ipynb 378 def patch(f=None, *, as_prop=False, cls_method=False): "Decorator: add `f` to the first parameter's class (based on f's type annotations)" if f is None: return partial(patch, as_prop=as_prop, cls_method=cls_method) @@ -961,19 +961,19 @@ def patch(f=None, *, as_prop=False, cls_method=False): cls = union2tuple(eval_type(ann.pop('cls') if cls_method else next(iter(ann.values())), glb, loc)) return patch_to(cls, as_prop=as_prop, cls_method=cls_method)(f) -# %% ../nbs/01_basics.ipynb 385 +# %% ../nbs/01_basics.ipynb 386 def patch_property(f): "Deprecated; use `patch(as_prop=True)` instead" warnings.warn("`patch_property` is deprecated and will be removed; use `patch(as_prop=True)` instead") cls = next(iter(f.__annotations__.values())) return patch_to(cls, as_prop=True)(f) -# %% ../nbs/01_basics.ipynb 387 +# %% ../nbs/01_basics.ipynb 390 def compile_re(pat): "Compile `pat` if it's not None" return None if pat is None else re.compile(pat) -# %% ../nbs/01_basics.ipynb 389 +# %% ../nbs/01_basics.ipynb 392 class ImportEnum(enum.Enum): "An `Enum` that can have its values imported" @classmethod @@ -981,17 +981,17 @@ def imports(cls): g = sys._getframe(1).f_locals for o in cls: g[o.name]=o -# %% ../nbs/01_basics.ipynb 392 +# %% ../nbs/01_basics.ipynb 395 class StrEnum(str,ImportEnum): "An `ImportEnum` that behaves like a `str`" def __str__(self): return self.name -# %% ../nbs/01_basics.ipynb 394 +# %% ../nbs/01_basics.ipynb 397 def str_enum(name, *vals): "Simplified creation of `StrEnum` types" return StrEnum(name, {o:o for o in vals}) -# %% ../nbs/01_basics.ipynb 396 +# %% ../nbs/01_basics.ipynb 399 class Stateful: "A base class/mixin for objects that should not serialize all their state" _stateattrs=() @@ -1011,12 +1011,12 @@ def _init_state(self): "Override for custom init and deserialization logic" self._state = {} -# %% ../nbs/01_basics.ipynb 402 +# %% ../nbs/01_basics.ipynb 405 class PrettyString(str): "Little hack to get strings to show properly in Jupyter." def __repr__(self): return self -# %% ../nbs/01_basics.ipynb 408 +# %% ../nbs/01_basics.ipynb 411 def even_mults(start, stop, n): "Build log-stepped array from `start` to `stop` in `n` steps." if n==1: return stop @@ -1024,7 +1024,7 @@ def even_mults(start, stop, n): step = mult**(1/(n-1)) return [start*(step**i) for i in range(n)] -# %% ../nbs/01_basics.ipynb 410 +# %% ../nbs/01_basics.ipynb 413 def num_cpus(): "Get number of cpus" try: return len(os.sched_getaffinity(0)) @@ -1032,16 +1032,16 @@ def num_cpus(): defaults.cpus = num_cpus() -# %% ../nbs/01_basics.ipynb 412 +# %% ../nbs/01_basics.ipynb 415 def add_props(f, g=None, n=2): "Create properties passing each of `range(n)` to f" if g is None: return (property(partial(f,i)) for i in range(n)) return (property(partial(f,i), partial(g,i)) for i in range(n)) -# %% ../nbs/01_basics.ipynb 415 +# %% ../nbs/01_basics.ipynb 418 def _typeerr(arg, val, typ): return TypeError(f"{arg}=={val} not {typ}") -# %% ../nbs/01_basics.ipynb 416 +# %% ../nbs/01_basics.ipynb 419 def typed(f): "Decorator to check param and return types at runtime" names = f.__code__.co_varnames @@ -1058,7 +1058,7 @@ def _f(*args,**kwargs): return res return functools.update_wrapper(_f, f) -# %% ../nbs/01_basics.ipynb 424 +# %% ../nbs/01_basics.ipynb 427 def exec_new(code): "Execute `code` in a new environment and return it" pkg = None if __name__=='__main__' else Path().cwd().name @@ -1066,13 +1066,13 @@ def exec_new(code): exec(code, g) return g -# %% ../nbs/01_basics.ipynb 426 +# %% ../nbs/01_basics.ipynb 429 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 427 +# %% ../nbs/01_basics.ipynb 430 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) diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 8bb30b04..2a8c7ffa 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -2644,14 +2644,24 @@ "text/markdown": [ "---\n", "\n", - "### GetAttr\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L485){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### GetAttr\n", "\n", "> GetAttr ()\n", "\n", "Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`" ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L485){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### GetAttr\n", + "\n", + "> GetAttr ()\n", + "\n", + "Inherit from this to have all attr accesses in `self._xtra` passed down to `self.default`" ] }, "execution_count": null, @@ -4075,14 +4085,24 @@ "text/markdown": [ "---\n", "\n", - "### fastuple\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L767){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### fastuple\n", "\n", "> fastuple (x=None, *rest)\n", "\n", "A `tuple` with elementwise ops and more friendly __init__ behavior" ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L767){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### fastuple\n", + "\n", + "> fastuple (x=None, *rest)\n", + "\n", + "A `tuple` with elementwise ops and more friendly __init__ behavior" ] }, "execution_count": null, @@ -4150,14 +4170,24 @@ "text/markdown": [ "---\n", "\n", - "#### fastuple.add\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L786){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "##### fastuple.add\n", "\n", "> fastuple.add (*args)\n", "\n", "`+` is already defined in `tuple` for concat, so use `add` instead" ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L786){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "##### fastuple.add\n", + "\n", + "> fastuple.add (*args)\n", + "\n", + "`+` is already defined in `tuple` for concat, so use `add` instead" ] }, "execution_count": null, @@ -4190,14 +4220,24 @@ "text/markdown": [ "---\n", "\n", - "#### fastuple.mul\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L782){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "##### fastuple.mul\n", "\n", "> fastuple.mul (*args)\n", "\n", "`*` is already defined in `tuple` for replicating, so use `mul` instead" ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L782){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "##### fastuple.mul\n", + "\n", + "> fastuple.mul (*args)\n", + "\n", + "`*` is already defined in `tuple` for replicating, so use `mul` instead" ] }, "execution_count": null, @@ -4341,6 +4381,8 @@ "text/markdown": [ "---\n", "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L813){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", "### bind\n", "\n", "> bind (func, *pargs, **pkwargs)\n", @@ -4348,7 +4390,15 @@ "Same as `partial`, except you can use `arg0` `arg1` etc param placeholders" ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L813){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "### bind\n", + "\n", + "> bind (func, *pargs, **pkwargs)\n", + "\n", + "Same as `partial`, except you can use `arg0` `arg1` etc param placeholders" ] }, "execution_count": null, @@ -4961,6 +5011,17 @@ "test_eq(copy_func(g)(4), 7)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class _clsmethod:\n", + " def __init__(self, f): self.f = f\n", + " def __get__(self, _, f_cls): return MethodType(self.f, f_cls)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -4979,7 +5040,7 @@ " for o in functools.WRAPPER_ASSIGNMENTS: setattr(nf, o, getattr(f,o))\n", " nf.__qualname__ = f\"{c_.__name__}.{nm}\"\n", " if cls_method:\n", - " setattr(c_, nm, MethodType(nf, c_))\n", + " setattr(c_, nm, _clsmethod(nf))\n", " else:\n", " setattr(c_, nm, property(nf) if as_prop else nf)\n", " # Avoid clobbering existing functions\n", @@ -5211,6 +5272,33 @@ " return patch_to(cls, as_prop=True)(f)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Patching `classmethod` shouldn't affect how python's inheritance works" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FastParent: pass\n", + "\n", + "@patch(cls_method=True)\n", + "def type_cls(cls: FastParent): return cls\n", + "\n", + "class FastChild(FastParent): pass\n", + "\n", + "parent = FastParent()\n", + "test_eq(parent.type_cls(), FastParent)\n", + "\n", + "child = FastChild()\n", + "test_eq(child.type_cls(), FastChild)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -5265,7 +5353,9 @@ "text/markdown": [ "---\n", "\n", - "### ImportEnum\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L977){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### ImportEnum\n", "\n", "> ImportEnum (value, names=None, module=None, qualname=None, type=None,\n", "> start=1)\n", @@ -5273,7 +5363,16 @@ "An `Enum` that can have its values imported" ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L977){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### ImportEnum\n", + "\n", + "> ImportEnum (value, names=None, module=None, qualname=None, type=None,\n", + "> start=1)\n", + "\n", + "An `Enum` that can have its values imported" ] }, "execution_count": null, @@ -5319,7 +5418,9 @@ "text/markdown": [ "---\n", "\n", - "### StrEnum\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L985){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### StrEnum\n", "\n", "> StrEnum (value, names=None, module=None, qualname=None, type=None,\n", "> start=1)\n", @@ -5327,7 +5428,16 @@ "An `ImportEnum` that behaves like a `str`" ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L985){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### StrEnum\n", + "\n", + "> StrEnum (value, names=None, module=None, qualname=None, type=None,\n", + "> start=1)\n", + "\n", + "An `ImportEnum` that behaves like a `str`" ] }, "execution_count": null, @@ -5409,14 +5519,24 @@ "text/markdown": [ "---\n", "\n", - "### Stateful\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L995){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### Stateful\n", "\n", "> Stateful (*args, **kwargs)\n", "\n", "A base class/mixin for objects that should not serialize all their state" ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L995){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### Stateful\n", + "\n", + "> Stateful (*args, **kwargs)\n", + "\n", + "A base class/mixin for objects that should not serialize all their state" ] }, "execution_count": null, @@ -5524,14 +5644,24 @@ "text/markdown": [ "---\n", "\n", - "### PrettyString\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1015){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### PrettyString\n", "\n", "\n", "\n", "Little hack to get strings to show properly in Jupyter." ], "text/plain": [ - "" + "---\n", + "\n", + "[source](https://github.com/fastai/fastcore/blob/master/fastcore/basics.py#L1015){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n", + "\n", + "#### PrettyString\n", + "\n", + "\n", + "\n", + "Little hack to get strings to show properly in Jupyter." ] }, "execution_count": null, @@ -5650,7 +5780,7 @@ { "data": { "text/plain": [ - "4" + "8" ] }, "execution_count": null, @@ -5918,14 +6048,20 @@ "text/markdown": [ "---\n", "\n", - "#### ipython_shell\n", + "### ipython_shell\n", "\n", "> ipython_shell ()\n", "\n", "Same as `get_ipython` but returns `False` if not in IPython" ], "text/plain": [ - "" + "---\n", + "\n", + "### ipython_shell\n", + "\n", + "> ipython_shell ()\n", + "\n", + "Same as `get_ipython` but returns `False` if not in IPython" ] }, "execution_count": null, @@ -5947,14 +6083,20 @@ "text/markdown": [ "---\n", "\n", - "#### in_ipython\n", + "### in_ipython\n", "\n", "> in_ipython ()\n", "\n", "Check if code is running in some kind of IPython environment" ], "text/plain": [ - "" + "---\n", + "\n", + "### in_ipython\n", + "\n", + "> in_ipython ()\n", + "\n", + "Check if code is running in some kind of IPython environment" ] }, "execution_count": null, @@ -5976,14 +6118,20 @@ "text/markdown": [ "---\n", "\n", - "#### in_colab\n", + "### in_colab\n", "\n", "> in_colab ()\n", "\n", "Check if the code is running in Google Colaboratory" ], "text/plain": [ - "" + "---\n", + "\n", + "### in_colab\n", + "\n", + "> in_colab ()\n", + "\n", + "Check if the code is running in Google Colaboratory" ] }, "execution_count": null, @@ -6005,14 +6153,20 @@ "text/markdown": [ "---\n", "\n", - "#### in_jupyter\n", + "### in_jupyter\n", "\n", "> in_jupyter ()\n", "\n", "Check if the code is running in a jupyter notebook" ], "text/plain": [ - "" + "---\n", + "\n", + "### in_jupyter\n", + "\n", + "> in_jupyter ()\n", + "\n", + "Check if the code is running in a jupyter notebook" ] }, "execution_count": null, @@ -6034,14 +6188,20 @@ "text/markdown": [ "---\n", "\n", - "#### in_notebook\n", + "### in_notebook\n", "\n", "> in_notebook ()\n", "\n", "Check if the code is running in a jupyter notebook" ], "text/plain": [ - "" + "---\n", + "\n", + "### in_notebook\n", + "\n", + "> in_notebook ()\n", + "\n", + "Check if the code is running in a jupyter notebook" ] }, "execution_count": null, @@ -6110,7 +6270,7 @@ "split_at_heading": true }, "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" } From 93c6d9c74f22f2f39c214cf6166699c9aad26f10 Mon Sep 17 00:00:00 2001 From: Salehbigdeli Date: Thu, 2 Mar 2023 04:12:08 +0100 Subject: [PATCH 2/2] export new class --- fastcore/_modidx.py | 3 +++ fastcore/basics.py | 5 +++++ nbs/01_basics.ipynb | 1 + 3 files changed, 9 insertions(+) diff --git a/fastcore/_modidx.py b/fastcore/_modidx.py index 1498fdc5..30405324 100644 --- a/fastcore/_modidx.py +++ b/fastcore/_modidx.py @@ -62,6 +62,9 @@ 'fastcore.basics._SelfCls.__getattr__': ('basics.html#_selfcls.__getattr__', 'fastcore/basics.py'), 'fastcore.basics._SelfCls.__getitem__': ('basics.html#_selfcls.__getitem__', 'fastcore/basics.py'), 'fastcore.basics._access': ('basics.html#_access', 'fastcore/basics.py'), + 'fastcore.basics._clsmethod': ('basics.html#_clsmethod', 'fastcore/basics.py'), + 'fastcore.basics._clsmethod.__get__': ('basics.html#_clsmethod.__get__', 'fastcore/basics.py'), + 'fastcore.basics._clsmethod.__init__': ('basics.html#_clsmethod.__init__', 'fastcore/basics.py'), 'fastcore.basics._eval_type': ('basics.html#_eval_type', 'fastcore/basics.py'), 'fastcore.basics._get_op': ('basics.html#_get_op', 'fastcore/basics.py'), 'fastcore.basics._ispy3_10': ('basics.html#_ispy3_10', 'fastcore/basics.py'), diff --git a/fastcore/basics.py b/fastcore/basics.py index d8760fee..7ff38d26 100644 --- a/fastcore/basics.py +++ b/fastcore/basics.py @@ -934,6 +934,11 @@ def copy_func(f): fn.__qualname__ = f.__qualname__ return fn +# %% ../nbs/01_basics.ipynb 366 +class _clsmethod: + def __init__(self, f): self.f = f + def __get__(self, _, f_cls): return MethodType(self.f, f_cls) + # %% ../nbs/01_basics.ipynb 367 def patch_to(cls, as_prop=False, cls_method=False): "Decorator: add `f` to `cls`" diff --git a/nbs/01_basics.ipynb b/nbs/01_basics.ipynb index 2a8c7ffa..f754b133 100644 --- a/nbs/01_basics.ipynb +++ b/nbs/01_basics.ipynb @@ -5017,6 +5017,7 @@ "metadata": {}, "outputs": [], "source": [ + "#|export\n", "class _clsmethod:\n", " def __init__(self, f): self.f = f\n", " def __get__(self, _, f_cls): return MethodType(self.f, f_cls)"