From ceda4d7f96d3c2e133a0194ca2bcbf3e772b6afd Mon Sep 17 00:00:00 2001 From: Filippo Vicentini Date: Mon, 6 Jun 2022 23:23:33 +0200 Subject: [PATCH 1/2] use slots for plum.Type so that the gc parent of _type is the Type instance itself and not Type().__dict__ --- plum/type.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plum/type.py b/plum/type.py index 2e3bd810..4046c030 100644 --- a/plum/type.py +++ b/plum/type.py @@ -75,6 +75,8 @@ class VarArgs(AbstractType): number of types. Defaults to `object`. """ + __slots__ = ["type"] + def __init__(self, type=object): self.type = ptype(type) @@ -159,6 +161,8 @@ class Union(ComparableType): alias (str, optional): Give the union a name. """ + __slots__ = ["_types"] + def __init__(self, *types, alias=None): # Lazily convert to a set to avoid resolution errors. self._types = tuple(ptype(t) for t in types) @@ -220,6 +224,8 @@ class Type(ComparableType): type (type): Type to encapsulate. """ + __slots__ = ["_type"] + def __init__(self, type): self._type = type From 868215797177aed5d2005d286971650e4c4b139c Mon Sep 17 00:00:00 2001 From: Filippo Vicentini Date: Wed, 8 Jun 2022 10:38:17 +0200 Subject: [PATCH 2/2] Add autoreload support --- README.md | 26 ++++++++++++ plum/__init__.py | 2 + plum/autoreload.py | 87 ++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + tests/test_autoreload.py | 60 +++++++++++++++++++++++++++ 5 files changed, 176 insertions(+) create mode 100644 plum/autoreload.py create mode 100644 tests/test_autoreload.py diff --git a/README.md b/README.md index 5139f524..fe2dece5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Everybody likes multiple dispatch, just like everybody likes plums. - [Add Multiple Methods](#add-multiple-methods) - [Extend a Function From Another Package](#extend-a-function-from-another-package) - [Directly Invoke a Method](#directly-invoke-a-method) + * [IPython's autoreload support](#support-for-ipython-autoreload) ## Installation @@ -1165,3 +1166,28 @@ def f(x: str): >>> f.invoke(str)(1) 'str' ``` + +### Support for IPython autoreload + +Plum does not work out of the box with IPython's autoreload, and if you reload a file where a class is defined, you will most likely break your dispatch table. + +However, experimental support for IPython's autoreload is included into plum but it is not enabled by default, as it overrides some internal methods of IPython. +To activate it, either set the environment variable `PLUM_AUTORELOAD=1` **before** loading plum + +```bash +export PLUM_AUTORELOAD=1 +``` + +or manually call the `autoreload.activate` method in an interactive session. + +```python +import plum +plum.autoreload.activate() +``` + +If there are issues with autoreload, please open a bug report. + + + + + diff --git a/plum/__init__.py b/plum/__init__.py index 300b14cc..4a75f56e 100644 --- a/plum/__init__.py +++ b/plum/__init__.py @@ -25,3 +25,5 @@ def _function_dispatch(*args, **kw_args): from .resolvable import * from .signature import * from .type import * + +from . import autoreload diff --git a/plum/autoreload.py b/plum/autoreload.py new file mode 100644 index 00000000..975cb272 --- /dev/null +++ b/plum/autoreload.py @@ -0,0 +1,87 @@ +import gc +import os + +from .type import Type, Union +from .dispatcher import Dispatcher + +__all__ = ["activate", "deactivate"] + + +def _update_instances(old, new): + """Use garbage collector to find all instances that refer to the old + class definition and update their __class__ to point to the new class + definition""" + + refs = gc.get_referrers(old) + + updated_plum_type = False + + for ref in refs: + if type(ref) is old: + ref.__class__ = new + elif type(ref) == Type: + updated_plum_type = True + ref._type = new + + # if we updated a plum type, then + # use the gc to get all dispatchers and clear + # their cache + if updated_plum_type: + refs = gc.get_referrers(Dispatcher) + for ref in refs: + if type(ref) is Dispatcher: + ref.clear_cache() + + +_update_instances_original = None + + +def activate(): + """ + Pirate autoreload's `update_instance` function to handle Plum types. + """ + from IPython.extensions import autoreload + from IPython.extensions.autoreload import update_instances + + # First, cache the original method so we can deactivate ourselves. + global _update_instances_original + if _update_instances_original is None: + _update_instances_original = autoreload.update_instances + + # Then, override the update_instance method + setattr(autoreload, "update_instances", _update_instances) + + +def deactivate(): + """ + Disable Plum's autoreload hack. + """ + global _update_instances_original + if _update_instances_original is None: # pragma: no cover + raise RuntimeError("Plum Autoreload module was never activated.") + + from IPython.extensions import autoreload + + setattr(autoreload, "update_instances", _update_instances_original) + + +# Detect `PLUM_AUTORELOAD` env variable +_autoload = os.environ.get("PLUM_AUTORELOAD", "0").lower() +if _autoload in ("y", "yes", "t", "true", "on", "1"): # pragma: no cover + _autoload = True +else: + _autoload = False + +if _autoload: # pragma: no cover + try: + # Try to load IPython and get the iPython session, but don't crash if + # this does not work (for example IPython not installed, or python shell) + from IPython import get_ipython + + ip = get_ipython() + if ip is not None: + if "IPython.extensions.storemagic" in ip.extension_manager.loaded: + activate() + + except ImportError: + pass diff --git a/requirements.txt b/requirements.txt index 3158cc2d..37bbffea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ black==22.3.0 pre-commit setuptools_scm[toml] setuptools_scm_git_archive +IPython \ No newline at end of file diff --git a/tests/test_autoreload.py b/tests/test_autoreload.py new file mode 100644 index 00000000..b19003a9 --- /dev/null +++ b/tests/test_autoreload.py @@ -0,0 +1,60 @@ +import numpy as np +import pytest + +from pathlib import Path + +from plum import Dispatcher, autoreload as p_autoreload +from plum.function import NotFoundLookupError + + +def test_autoreload_activate_deactivate(): + p_autoreload.activate() + + assert p_autoreload._update_instances_original is not None + assert ( + p_autoreload._update_instances_original.__module__ + == "IPython.extensions.autoreload" + ) + + from IPython.extensions import autoreload + + assert autoreload.update_instances.__module__ == "plum.autoreload" + + p_autoreload.deactivate() + + assert ( + p_autoreload._update_instances_original.__module__ + == "IPython.extensions.autoreload" + ) + assert autoreload.update_instances.__module__ == "IPython.extensions.autoreload" + assert autoreload.update_instances == p_autoreload._update_instances_original + + +def test_autoreload_works(): + dispatch = Dispatcher() + + class A1: + pass + + class A2: + pass + + @dispatch + def test(x: A1): + return 1 + + assert test(A1()) == 1 + + with pytest.raises(NotFoundLookupError): + test(A2()) + + a1 = A1() + + p_autoreload._update_instances(A1, A2) + + assert test(A2()) == 1 + + with pytest.raises(NotFoundLookupError): + test(A1()) + + assert isinstance(a1, A2)