From ddf5ebe557697a1454ec030c4c7277af3241d5a6 Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:47:31 +0530 Subject: [PATCH] fix: keep isinstance(fig, go.Figure) working after register_plotly_resampler (#364) register_plotly_resampler replaced go.Figure and go.FigureWidget with plain functions, so isinstance(fig, go.Figure) raised a TypeError. Replace them with classes whose metaclass forwards isinstance, issubclass and attribute lookups to the original plotly class. Fixes #364 --- CHANGELOG.md | 3 +++ plotly_resampler/registering.py | 38 ++++++++++++++++++++++++------- tests/test_registering.py | 40 ++++++++++++++++++++++++--------- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4689455b..4d8b7485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Latest +## What's Changed +* :bug: keep `isinstance(fig, go.Figure)` and `isinstance(fig, go.FigureWidget)` working after `register_plotly_resampler()` (#364) by @raashish1601 + # v0.11.1 (2026-10-04) The README now points to [FlexViz](https://github.com/flex-analytics/flexviz), our successor project for whole-dashboard cross-filtering at scale. diff --git a/plotly_resampler/registering.py b/plotly_resampler/registering.py index 36536ca4..137e60c5 100644 --- a/plotly_resampler/registering.py +++ b/plotly_resampler/registering.py @@ -2,8 +2,6 @@ __author__ = "Jeroen Van Der Donckt, Jonas Van Der Donckt, Emiel Deprost" -from functools import wraps - import plotly from plotly_resampler import FigureResampler, FigureWidgetResampler @@ -59,6 +57,24 @@ def _is_ipython_env(): return False +class _WrappedConstrMeta(type): + """Metaclass of the wrapped constructors. + + Delegates ``isinstance`` and ``issubclass`` checks to the original plotly class, so + that e.g. ``isinstance(fig, go.Figure)`` keeps working once registered. + """ + + def __instancecheck__(cls, instance): + return isinstance(instance, cls.__wrapped__) + + def __subclasscheck__(cls, subclass): + return issubclass(subclass, cls.__wrapped__) + + def __getattr__(cls, name): + # Look up class attributes (e.g., static methods) on the original class + return getattr(cls.__wrapped__, name) + + def _register_wrapper( module: type, constr_name: str, @@ -68,14 +84,20 @@ def _register_wrapper( constr = getattr(module, constr_name) constr = _get_plotly_constr(constr) # get the original plotly constructor - # print(f"Wrapping {constr_name} with {pr_class}") - - @wraps(constr) - def wrapped_constr(*args, **kwargs): - # print(f"Executing constructor wrapper for {constr_name}", constr) + def __new__(cls, *args, **kwargs): + # The returned object is not an instance of `cls`, so `__init__` is not called return pr_class(constr(*args, **kwargs), **aggregator_kwargs) - wrapped_constr.__name__ = WRAPPED_PREFIX + constr_name + wrapped_constr = _WrappedConstrMeta( + WRAPPED_PREFIX + constr_name, + (), + { + "__new__": __new__, + "__wrapped__": constr, + "__doc__": constr.__doc__, + "__module__": constr.__module__, + }, + ) setattr(module, constr_name, wrapped_constr) diff --git a/tests/test_registering.py b/tests/test_registering.py index b2b8ac95..d2a3bd93 100644 --- a/tests/test_registering.py +++ b/tests/test_registering.py @@ -1,5 +1,3 @@ -from inspect import isfunction - import numpy as np import plotly.express as px import plotly.graph_objects as go @@ -9,6 +7,7 @@ AbstractFigureAggregator, ) from plotly_resampler.registering import ( + _already_wrapped, _get_plotly_constr, register_plotly_resampler, unregister_plotly_resampler, @@ -23,14 +22,14 @@ def test_get_plotly_const(registering_cleanup): assert issubclass(FigureWidgetResampler, AbstractFigureAggregator) # Is unregistered now - assert not (isfunction(go.Figure) or isfunction(go.FigureWidget)) + assert not (_already_wrapped(go.Figure) or _already_wrapped(go.FigureWidget)) assert not issubclass(go.Figure, AbstractFigureAggregator) assert not issubclass(go.FigureWidget, AbstractFigureAggregator) assert not issubclass(_get_plotly_constr(go.Figure), AbstractFigureAggregator) assert not issubclass(_get_plotly_constr(go.FigureWidget), AbstractFigureAggregator) register_plotly_resampler() - assert isfunction(go.Figure) and isfunction(go.FigureWidget) + assert _already_wrapped(go.Figure) and _already_wrapped(go.FigureWidget) assert isinstance(go.Figure(), AbstractFigureAggregator) assert isinstance(go.FigureWidget(), AbstractFigureAggregator) assert issubclass(FigureResampler, AbstractFigureAggregator) @@ -39,25 +38,44 @@ def test_get_plotly_const(registering_cleanup): assert not issubclass(_get_plotly_constr(go.FigureWidget), AbstractFigureAggregator) unregister_plotly_resampler() - assert not (isfunction(go.Figure) or isfunction(go.FigureWidget)) + assert not (_already_wrapped(go.Figure) or _already_wrapped(go.FigureWidget)) assert not issubclass(go.Figure, AbstractFigureAggregator) assert not issubclass(go.FigureWidget, AbstractFigureAggregator) assert not issubclass(_get_plotly_constr(go.Figure), AbstractFigureAggregator) assert not issubclass(_get_plotly_constr(go.FigureWidget), AbstractFigureAggregator) +def test_isinstance_when_registered(registering_cleanup): + register_plotly_resampler(mode="figure") + fig = go.Figure() + assert isinstance(fig, FigureResampler) + assert isinstance(fig, go.Figure) + assert not isinstance(fig, go.FigureWidget) + assert issubclass(FigureResampler, go.Figure) + assert not issubclass(FigureResampler, go.FigureWidget) + + register_plotly_resampler(mode="widget") + fig = go.FigureWidget() + assert isinstance(fig, FigureWidgetResampler) + assert isinstance(fig, go.FigureWidget) + assert isinstance(fig, go.Figure) is isinstance(fig, _get_plotly_constr(go.Figure)) + + unregister_plotly_resampler() + assert isinstance(fig, go.FigureWidget) + + def test_register_and_unregister_graph_objects(registering_cleanup): import plotly.graph_objects as go_ # Is unregistered now - assert not (isfunction(go_.Figure) or isfunction(go_.FigureWidget)) + assert not (_already_wrapped(go_.Figure) or _already_wrapped(go_.FigureWidget)) fig = go_.Figure() assert not isinstance(fig, AbstractFigureAggregator) fig = go_.FigureWidget() assert not isinstance(fig, AbstractFigureAggregator) register_plotly_resampler() - assert isfunction(go_.Figure) and isfunction(go_.FigureWidget) + assert _already_wrapped(go_.Figure) and _already_wrapped(go_.FigureWidget) fig = go_.Figure() assert isinstance(fig, AbstractFigureAggregator) assert isinstance(fig, FigureResampler) @@ -68,7 +86,7 @@ def test_register_and_unregister_graph_objects(registering_cleanup): assert not isinstance(fig, FigureResampler) unregister_plotly_resampler() - assert not (isfunction(go_.Figure) or isfunction(go_.FigureWidget)) + assert not (_already_wrapped(go_.Figure) or _already_wrapped(go_.FigureWidget)) fig = go_.Figure() assert not isinstance(fig, AbstractFigureAggregator) fig = go_.FigureWidget() @@ -79,14 +97,14 @@ def test_register_and_unregister_graph_objs(registering_cleanup): import plotly.graph_objs as go_ # Is unregistered now - assert not (isfunction(go_.Figure) or isfunction(go_.FigureWidget)) + assert not (_already_wrapped(go_.Figure) or _already_wrapped(go_.FigureWidget)) fig = go_.Figure() assert not isinstance(fig, AbstractFigureAggregator) fig = go_.FigureWidget() assert not isinstance(fig, AbstractFigureAggregator) register_plotly_resampler() - assert isfunction(go_.Figure) and isfunction(go_.FigureWidget) + assert _already_wrapped(go_.Figure) and _already_wrapped(go_.FigureWidget) fig = go_.Figure() assert isinstance(fig, AbstractFigureAggregator) assert isinstance(fig, FigureResampler) @@ -97,7 +115,7 @@ def test_register_and_unregister_graph_objs(registering_cleanup): assert not isinstance(fig, FigureResampler) unregister_plotly_resampler() - assert not (isfunction(go_.Figure) or isfunction(go_.FigureWidget)) + assert not (_already_wrapped(go_.Figure) or _already_wrapped(go_.FigureWidget)) fig = go_.Figure() assert not isinstance(fig, AbstractFigureAggregator) fig = go_.FigureWidget()