From 6ebc63d7cda104867674159920e44cd8a62689fe Mon Sep 17 00:00:00 2001 From: Priyansh Date: Tue, 27 Oct 2026 18:53:43 +0000 Subject: [PATCH 1/2] Deduplicate optional FigureWidget import in codegen The code generator emitted the optional FigureWidget import block (try: import ipywidgets / missing_* fallback) into both plotly/graph_objs/__init__.py and plotly/graph_objects/__init__.py, producing an identical block at the end of each generated file. graph_objects already re-exports every other class from graph_objs (including Figure), and graph_objs owns the optional ipywidgets import and the missing_* fallback. Emit the block only for graph_objs and re-export FigureWidget from graph_objs in graph_objects like every other class. Behaviour is unchanged: plotly.graph_objects.FigureWidget still resolves to the real class when ipywidgets is present and to the missing_* fallback when it is absent. Add a regression test proving the invariant (identity, forced-failure fallback, and no duplicated ipywidgets block in graph_objects). Fixes #5849 --- codegen/__init__.py | 6 +- plotly/graph_objects/__init__.py | 39 +--------- .../test_figurewidget_reexport.py | 77 +++++++++++++++++++ 3 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 tests/test_core/test_figure_widget_backend/test_figurewidget_reexport.py diff --git a/codegen/__init__.py b/codegen/__init__.py index b299fa36045..dea51a88664 100644 --- a/codegen/__init__.py +++ b/codegen/__init__.py @@ -307,6 +307,11 @@ def __getattr__(import_name): "..graph_objs." + rel_path.split(".")[-1] for rel_path in datatype_rel_class_imports[()] ] + # FigureWidget is owned by graph_objs, which handles the optional + # ipywidgets import and the missing_* fallback. Re-export it from + # graph_objs like every other class instead of duplicating the optional + # import block in graph_objects. + graph_objects_rel_classes.append("..graph_objs.FigureWidget") graph_objects_rel_modules = [ "..graph_objs." + rel_module.split(".")[-1] for rel_module in datatype_rel_module_imports[()] @@ -315,7 +320,6 @@ def __getattr__(import_name): graph_objects_init_source = build_from_imports_py( graph_objects_rel_modules, graph_objects_rel_classes, - init_extra=optional_figure_widget_import, ) graph_objects_path = opath.join(outdir, "graph_objects", "__init__.py") os.makedirs(opath.join(outdir, "graph_objects"), exist_ok=True) diff --git a/plotly/graph_objects/__init__.py b/plotly/graph_objects/__init__.py index 670e4b8c6a1..919f804085d 100644 --- a/plotly/graph_objects/__init__.py +++ b/plotly/graph_objects/__init__.py @@ -77,6 +77,7 @@ from ..graph_objs import YBins from ..graph_objs import Trace from ..graph_objs import Histogram2dcontour + from ..graph_objs import FigureWidget from ..graph_objs import waterfall from ..graph_objs import volume from ..graph_objs import violin @@ -255,42 +256,6 @@ "..graph_objs.YBins", "..graph_objs.Trace", "..graph_objs.Histogram2dcontour", + "..graph_objs.FigureWidget", ], ) - - -if sys.version_info < (3, 7) or TYPE_CHECKING: - try: - import ipywidgets as _ipywidgets - from packaging.version import Version as _Version - - if _Version(_ipywidgets.__version__) >= _Version("7.0.0"): - from ..graph_objs._figurewidget import FigureWidget - else: - raise ImportError() - except Exception: - from ..missing_anywidget import FigureWidget -else: - __all__.append("FigureWidget") - orig_getattr = __getattr__ - - def __getattr__(import_name): - if import_name == "FigureWidget": - try: - import ipywidgets - from packaging.version import Version - - if Version(ipywidgets.__version__) >= Version("7.0.0"): - from ..graph_objs._figurewidget import FigureWidget - - return FigureWidget - else: - raise ImportError() - except Exception: - from ..missing_anywidget import FigureWidget - - return FigureWidget - else: - raise ImportError() - - return orig_getattr(import_name) diff --git a/tests/test_core/test_figure_widget_backend/test_figurewidget_reexport.py b/tests/test_core/test_figure_widget_backend/test_figurewidget_reexport.py new file mode 100644 index 00000000000..1c801081c94 --- /dev/null +++ b/tests/test_core/test_figure_widget_backend/test_figurewidget_reexport.py @@ -0,0 +1,77 @@ +# ruff: noqa: F401 F403 F405 +""" +Regression test for plotly/plotly.py#5849. + +The optional ``FigureWidget`` import block (the ``try: import ipywidgets`` / +``missing_*`` fallback) must live only in ``plotly/graph_objs/__init__.py``. +``plotly/graph_objects/__init__.py`` must re-export ``FigureWidget`` from +``graph_objs`` like every other class, instead of duplicating that block. + +These tests fail on unmodified master, where the block is emitted into both +generated files. +""" + +import os +import os.path as opath +import subprocess +import sys + +import plotly +import plotly.graph_objects as go +import plotly.graph_objs as go2 +from plotly.missing_anywidget import FigureWidget as FigureWidgetMissingAnywidget + + +def test_figurewidget_is_reexported_from_graph_objs(): + # (a) graph_objects.FigureWidget is the same object as + # graph_objs.FigureWidget: it is re-exported, not imported separately. + assert go.FigureWidget is go2.FigureWidget + + +def test_figurewidget_fallback_when_ipywidgets_missing(): + # (b) With the ipywidgets import forced to fail, graph_objects.FigureWidget + # still resolves to the missing_* fallback owned by graph_objs. This runs + # in a subprocess so it holds whether or not ipywidgets is installed. + script = ( + "import sys\n" + "sys.modules['ipywidgets'] = None\n" + "import plotly.graph_objects as go\n" + "from plotly.missing_anywidget import FigureWidget as Missing\n" + "assert go.FigureWidget is Missing, " + "f'expected missing_* fallback, got {go.FigureWidget!r}'\n" + "print('ok')\n" + ) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + assert result.returncode == 0, ( + f"forced-failure fallback check failed:\n" + f"stdout: {result.stdout}\nstderr: {result.stderr}" + ) + assert "ok" in result.stdout + + +def test_no_duplicate_ipywidgets_block_in_graph_objects(): + # (c) The optional ipywidgets import block must not be duplicated in + # graph_objects/__init__.py; it belongs only in graph_objs/__init__.py. + plotly_root = opath.dirname(plotly.__file__) + go_init = opath.join(plotly_root, "graph_objects", "__init__.py") + with open(go_init) as f: + go_source = f.read() + assert "import ipywidgets" not in go_source, ( + "graph_objects/__init__.py still contains a duplicated " + "optional ipywidgets import block" + ) + assert "missing_anywidget" not in go_source, ( + "graph_objects/__init__.py still references the missing_* fallback " + "directly instead of re-exporting FigureWidget from graph_objs" + ) + + # The block must still be present in graph_objs/__init__.py. + go2_init = opath.join(plotly_root, "graph_objs", "__init__.py") + with open(go2_init) as f: + go2_source = f.read() + assert "import ipywidgets" in go2_source + assert "missing_anywidget" in go2_source From 61bb49e772e40078485168ba18af1dfd24d89c0a Mon Sep 17 00:00:00 2001 From: DawnofGenX Date: Wed, 11 Nov 2026 04:02:15 +0000 Subject: [PATCH 2/2] refactor(codegen): inline the FigureWidget re-export per review --- codegen/__init__.py | 13 ++-- .../test_figurewidget_reexport.py | 77 ------------------- 2 files changed, 6 insertions(+), 84 deletions(-) delete mode 100644 tests/test_core/test_figure_widget_backend/test_figurewidget_reexport.py diff --git a/codegen/__init__.py b/codegen/__init__.py index dea51a88664..57b7b2d6f24 100644 --- a/codegen/__init__.py +++ b/codegen/__init__.py @@ -304,14 +304,13 @@ def __getattr__(import_name): # Output graph_objects.py alias graph_objects_rel_classes = [ - "..graph_objs." + rel_path.split(".")[-1] - for rel_path in datatype_rel_class_imports[()] + *( + "..graph_objs." + rel_path.split(".")[-1] + for rel_path in datatype_rel_class_imports[()] + ), + # graph_objs owns the optional ipywidgets import and the fallback + "..graph_objs.FigureWidget", ] - # FigureWidget is owned by graph_objs, which handles the optional - # ipywidgets import and the missing_* fallback. Re-export it from - # graph_objs like every other class instead of duplicating the optional - # import block in graph_objects. - graph_objects_rel_classes.append("..graph_objs.FigureWidget") graph_objects_rel_modules = [ "..graph_objs." + rel_module.split(".")[-1] for rel_module in datatype_rel_module_imports[()] diff --git a/tests/test_core/test_figure_widget_backend/test_figurewidget_reexport.py b/tests/test_core/test_figure_widget_backend/test_figurewidget_reexport.py deleted file mode 100644 index 1c801081c94..00000000000 --- a/tests/test_core/test_figure_widget_backend/test_figurewidget_reexport.py +++ /dev/null @@ -1,77 +0,0 @@ -# ruff: noqa: F401 F403 F405 -""" -Regression test for plotly/plotly.py#5849. - -The optional ``FigureWidget`` import block (the ``try: import ipywidgets`` / -``missing_*`` fallback) must live only in ``plotly/graph_objs/__init__.py``. -``plotly/graph_objects/__init__.py`` must re-export ``FigureWidget`` from -``graph_objs`` like every other class, instead of duplicating that block. - -These tests fail on unmodified master, where the block is emitted into both -generated files. -""" - -import os -import os.path as opath -import subprocess -import sys - -import plotly -import plotly.graph_objects as go -import plotly.graph_objs as go2 -from plotly.missing_anywidget import FigureWidget as FigureWidgetMissingAnywidget - - -def test_figurewidget_is_reexported_from_graph_objs(): - # (a) graph_objects.FigureWidget is the same object as - # graph_objs.FigureWidget: it is re-exported, not imported separately. - assert go.FigureWidget is go2.FigureWidget - - -def test_figurewidget_fallback_when_ipywidgets_missing(): - # (b) With the ipywidgets import forced to fail, graph_objects.FigureWidget - # still resolves to the missing_* fallback owned by graph_objs. This runs - # in a subprocess so it holds whether or not ipywidgets is installed. - script = ( - "import sys\n" - "sys.modules['ipywidgets'] = None\n" - "import plotly.graph_objects as go\n" - "from plotly.missing_anywidget import FigureWidget as Missing\n" - "assert go.FigureWidget is Missing, " - "f'expected missing_* fallback, got {go.FigureWidget!r}'\n" - "print('ok')\n" - ) - result = subprocess.run( - [sys.executable, "-c", script], - capture_output=True, - text=True, - ) - assert result.returncode == 0, ( - f"forced-failure fallback check failed:\n" - f"stdout: {result.stdout}\nstderr: {result.stderr}" - ) - assert "ok" in result.stdout - - -def test_no_duplicate_ipywidgets_block_in_graph_objects(): - # (c) The optional ipywidgets import block must not be duplicated in - # graph_objects/__init__.py; it belongs only in graph_objs/__init__.py. - plotly_root = opath.dirname(plotly.__file__) - go_init = opath.join(plotly_root, "graph_objects", "__init__.py") - with open(go_init) as f: - go_source = f.read() - assert "import ipywidgets" not in go_source, ( - "graph_objects/__init__.py still contains a duplicated " - "optional ipywidgets import block" - ) - assert "missing_anywidget" not in go_source, ( - "graph_objects/__init__.py still references the missing_* fallback " - "directly instead of re-exporting FigureWidget from graph_objs" - ) - - # The block must still be present in graph_objs/__init__.py. - go2_init = opath.join(plotly_root, "graph_objs", "__init__.py") - with open(go2_init) as f: - go2_source = f.read() - assert "import ipywidgets" in go2_source - assert "missing_anywidget" in go2_source