Skip to content

Commit 6e5184b

Browse files
committed
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
1 parent fcc3c8e commit 6e5184b

4 files changed

Lines changed: 85 additions & 38 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
2222
- Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution!
2323
- Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura for the contribution!
2424
- Fix serialization of box/lasso select data for figures with multiple subplots [[#5710](https://github.com/plotly/plotly.py/pull/5710)], with thanks to @flpi-sun for the contribution!
25+
- Stop the code generator from writing a duplicate optional `FigureWidget` import block into both `plotly.graph_objs` and `plotly.graph_objects`; `plotly.graph_objects.FigureWidget` is now re-exported from `plotly.graph_objs` like every other class, with no change in behaviour [[#5849](https://github.com/plotly/plotly.py/issues/5849)]
2526

2627
## [7.1.0] - 2026-09-15
2728

‎codegen/__init__.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,11 @@ def __getattr__(import_name):
307307
"..graph_objs." + rel_path.split(".")[-1]
308308
for rel_path in datatype_rel_class_imports[()]
309309
]
310+
# FigureWidget is owned by graph_objs, which handles the optional
311+
# ipywidgets import and the missing_* fallback. Re-export it from
312+
# graph_objs like every other class instead of duplicating the optional
313+
# import block in graph_objects.
314+
graph_objects_rel_classes.append("..graph_objs.FigureWidget")
310315
graph_objects_rel_modules = [
311316
"..graph_objs." + rel_module.split(".")[-1]
312317
for rel_module in datatype_rel_module_imports[()]
@@ -315,7 +320,6 @@ def __getattr__(import_name):
315320
graph_objects_init_source = build_from_imports_py(
316321
graph_objects_rel_modules,
317322
graph_objects_rel_classes,
318-
init_extra=optional_figure_widget_import,
319323
)
320324
graph_objects_path = opath.join(outdir, "graph_objects", "__init__.py")
321325
os.makedirs(opath.join(outdir, "graph_objects"), exist_ok=True)

‎plotly/graph_objects/__init__.py‎

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
from ..graph_objs import YBins
7878
from ..graph_objs import Trace
7979
from ..graph_objs import Histogram2dcontour
80+
from ..graph_objs import FigureWidget
8081
from ..graph_objs import waterfall
8182
from ..graph_objs import volume
8283
from ..graph_objs import violin
@@ -255,42 +256,6 @@
255256
"..graph_objs.YBins",
256257
"..graph_objs.Trace",
257258
"..graph_objs.Histogram2dcontour",
259+
"..graph_objs.FigureWidget",
258260
],
259261
)
260-
261-
262-
if sys.version_info < (3, 7) or TYPE_CHECKING:
263-
try:
264-
import ipywidgets as _ipywidgets
265-
from packaging.version import Version as _Version
266-
267-
if _Version(_ipywidgets.__version__) >= _Version("7.0.0"):
268-
from ..graph_objs._figurewidget import FigureWidget
269-
else:
270-
raise ImportError()
271-
except Exception:
272-
from ..missing_anywidget import FigureWidget
273-
else:
274-
__all__.append("FigureWidget")
275-
orig_getattr = __getattr__
276-
277-
def __getattr__(import_name):
278-
if import_name == "FigureWidget":
279-
try:
280-
import ipywidgets
281-
from packaging.version import Version
282-
283-
if Version(ipywidgets.__version__) >= Version("7.0.0"):
284-
from ..graph_objs._figurewidget import FigureWidget
285-
286-
return FigureWidget
287-
else:
288-
raise ImportError()
289-
except Exception:
290-
from ..missing_anywidget import FigureWidget
291-
292-
return FigureWidget
293-
else:
294-
raise ImportError()
295-
296-
return orig_getattr(import_name)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# ruff: noqa: F401 F403 F405
2+
"""
3+
Regression test for plotly/plotly.py#5849.
4+
5+
The optional ``FigureWidget`` import block (the ``try: import ipywidgets`` /
6+
``missing_*`` fallback) must live only in ``plotly/graph_objs/__init__.py``.
7+
``plotly/graph_objects/__init__.py`` must re-export ``FigureWidget`` from
8+
``graph_objs`` like every other class, instead of duplicating that block.
9+
10+
These tests fail on unmodified master, where the block is emitted into both
11+
generated files.
12+
"""
13+
14+
import os
15+
import os.path as opath
16+
import subprocess
17+
import sys
18+
19+
import plotly
20+
import plotly.graph_objects as go
21+
import plotly.graph_objs as go2
22+
from plotly.missing_anywidget import FigureWidget as FigureWidgetMissingAnywidget
23+
24+
25+
def test_figurewidget_is_reexported_from_graph_objs():
26+
# (a) graph_objects.FigureWidget is the same object as
27+
# graph_objs.FigureWidget: it is re-exported, not imported separately.
28+
assert go.FigureWidget is go2.FigureWidget
29+
30+
31+
def test_figurewidget_fallback_when_ipywidgets_missing():
32+
# (b) With the ipywidgets import forced to fail, graph_objects.FigureWidget
33+
# still resolves to the missing_* fallback owned by graph_objs. This runs
34+
# in a subprocess so it holds whether or not ipywidgets is installed.
35+
script = (
36+
"import sys\n"
37+
"sys.modules['ipywidgets'] = None\n"
38+
"import plotly.graph_objects as go\n"
39+
"from plotly.missing_anywidget import FigureWidget as Missing\n"
40+
"assert go.FigureWidget is Missing, "
41+
"f'expected missing_* fallback, got {go.FigureWidget!r}'\n"
42+
"print('ok')\n"
43+
)
44+
result = subprocess.run(
45+
[sys.executable, "-c", script],
46+
capture_output=True,
47+
text=True,
48+
)
49+
assert result.returncode == 0, (
50+
f"forced-failure fallback check failed:\n"
51+
f"stdout: {result.stdout}\nstderr: {result.stderr}"
52+
)
53+
assert "ok" in result.stdout
54+
55+
56+
def test_no_duplicate_ipywidgets_block_in_graph_objects():
57+
# (c) The optional ipywidgets import block must not be duplicated in
58+
# graph_objects/__init__.py; it belongs only in graph_objs/__init__.py.
59+
plotly_root = opath.dirname(plotly.__file__)
60+
go_init = opath.join(plotly_root, "graph_objects", "__init__.py")
61+
with open(go_init) as f:
62+
go_source = f.read()
63+
assert "import ipywidgets" not in go_source, (
64+
"graph_objects/__init__.py still contains a duplicated "
65+
"optional ipywidgets import block"
66+
)
67+
assert "missing_anywidget" not in go_source, (
68+
"graph_objects/__init__.py still references the missing_* fallback "
69+
"directly instead of re-exporting FigureWidget from graph_objs"
70+
)
71+
72+
# The block must still be present in graph_objs/__init__.py.
73+
go2_init = opath.join(plotly_root, "graph_objs", "__init__.py")
74+
with open(go2_init) as f:
75+
go2_source = f.read()
76+
assert "import ipywidgets" in go2_source
77+
assert "missing_anywidget" in go2_source

0 commit comments

Comments
 (0)