Skip to content

Commit bfe0837

Browse files
committed
revise recommendations.py
1 parent 17095f7 commit bfe0837

File tree

1 file changed

+111
-39
lines changed

1 file changed

+111
-39
lines changed

plotly/recommendations.py

Lines changed: 111 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
go.Figure(data=[go.Scatter(y=[1, 2, 3])]) # may emit recommendation warnings
1717
"""
1818

19+
from functools import partial
20+
import inspect
1921
import os
2022
import warnings
2123

@@ -51,39 +53,102 @@ def enabled(self, value):
5153

5254

5355
# -----------------------------------------------------------------------------
54-
# Recommendation checkers (extensible list)
56+
# Property resolution (single place for "does this property exist")
5557
# -----------------------------------------------------------------------------
5658

5759

58-
def _check_scatter_xy_length(obj, context):
59-
"""Warn if a scatter trace has x or y with length >= 10."""
60-
if context != "trace":
61-
return
62-
if getattr(obj, "plotly_name", None) != "scatter":
63-
return
64-
for prop in ("x", "y"):
60+
def _targets(obj, context, prefix):
61+
"""Yield each object (layout or trace) that this prefix applies to."""
62+
if context == "figure":
63+
if prefix == "layout" and hasattr(obj, "layout"):
64+
yield obj.layout
65+
if hasattr(obj, "data"):
66+
for t in obj.data:
67+
if prefix == "trace" or getattr(t, "plotly_name", None) == prefix:
68+
yield t
69+
elif (context == "trace" or context == "layout") and (
70+
prefix == "trace" or prefix == "layout" or getattr(obj, "plotly_name", None) == prefix
71+
):
72+
yield obj
73+
74+
75+
def _get_stacklevel():
76+
"""Find the first frame outside the plotly package so the warning points at user code."""
77+
plotly_dir = os.path.abspath(os.path.dirname(__file__))
78+
stack = inspect.stack()
79+
for i, frame in enumerate(stack):
6580
try:
66-
val = obj[prop]
67-
if val is not None and hasattr(val, "__len__") and len(val) >= 10:
68-
warnings.warn(
69-
"Scatter trace '%s' has length %d (recommended < 10)."
70-
% (prop, len(val)),
71-
UserWarning,
72-
stacklevel=2,
73-
)
74-
except (KeyError, TypeError):
75-
pass
81+
frame_path = os.path.abspath(frame.filename)
82+
except (AttributeError, TypeError):
83+
frame_path = getattr(frame, "filename", "") or ""
84+
if not frame_path.startswith(plotly_dir):
85+
break
86+
else:
87+
i = len(stack) - 1
88+
return i
89+
90+
91+
def _is_empty(obj):
92+
"""
93+
Quick check if the object has no values assigned yet (e.g. just constructed).
94+
Figure: no traces. Trace/layout: no properties in _props.
95+
"""
96+
if hasattr(obj, "_data"):
97+
return len(obj._data) == 0
98+
if hasattr(obj, "_props"):
99+
props = obj._props
100+
return props is None or len(props) == 0
101+
return False
102+
103+
104+
def _get_value(whole_obj, path, context):
105+
"""
106+
Resolve a full path (e.g. "scatter.x", "layout.title.text") from the whole
107+
object (figure, or current trace/layout). Returns a list of values, one per
108+
applicable target; missing properties yield None.
109+
"""
110+
prefix, suffix = path.split(".", 1) if "." in path else (path, "")
111+
result = []
112+
for target in _targets(whole_obj, context, prefix):
113+
if not suffix:
114+
result.append(target)
115+
continue
116+
try:
117+
v = target
118+
for p in suffix.split("."):
119+
v = v[p]
120+
result.append(v)
121+
except (KeyError, TypeError, AttributeError):
122+
result.append(None)
123+
return result
124+
125+
126+
# -----------------------------------------------------------------------------
127+
# Recommendation checkers (extensible list)
128+
# -----------------------------------------------------------------------------
129+
130+
131+
def max_length(input_list, max_length):
132+
"""
133+
Check if input_list has length >= max_length (e.g. recommended to keep shorter).
134+
Returns a string describing the issue, or None if no issue was found.
135+
"""
136+
if input_list is not None and hasattr(input_list, "__len__") and len(input_list) >= max_length:
137+
return f"has length {len(input_list)} (recommended <= {max_length})."
138+
return None
76139

77140

78141
def _recommendation_checkers():
79142
"""
80-
Return the list of (context_filter, checker_func) to run.
81-
checker_func(obj, context) may call warnings.warn().
82-
context is one of "figure", "trace", "layout".
83-
context_filter: set of contexts this checker applies to, or None for all.
143+
Return the list of (path_def, checker_func).
144+
145+
path_def: a dot-separated path string (e.g. "scatter.x") or list of same.
146+
checker_func: called with one value per path (or None if missing). Returns
147+
an issue string to warn about, or None.
84148
"""
85149
return [
86-
({"trace"}, _check_scatter_xy_length),
150+
("scatter.x", partial(max_length, max_length=1000)),
151+
("scatter.y", partial(max_length, max_length=1000)),
87152
]
88153

89154

@@ -93,32 +158,39 @@ def run_recommendations(obj, context):
93158
Called internally after Figure/trace/Layout construction when
94159
recommendations mode is enabled.
95160
161+
Property resolution is done here: for each checker we resolve the
162+
path(s) on the applicable object(s), pass the values (or None) to the
163+
checker, and catch exceptions so one checker cannot break others.
164+
96165
Parameters
97166
----------
98167
obj : BaseFigure | BasePlotlyType
99168
The constructed figure, trace, or layout.
100169
context : str
101170
One of "figure", "trace", "layout".
102171
"""
103-
if not config.enabled:
172+
173+
if (not config.enabled) or _is_empty(obj):
104174
return
175+
105176
checkers = _recommendation_checkers()
106-
# For figures, run trace checkers on each trace (traces have props set by then)
107-
if context == "figure" and hasattr(obj, "data"):
108-
for trace in obj.data:
109-
for ctx_filter, checker in checkers:
110-
if ctx_filter is not None and "trace" not in ctx_filter:
111-
continue
112-
try:
113-
checker(trace, "trace")
114-
except Exception:
115-
pass
116-
# Run checkers for this context
117-
for ctx_filter, checker in checkers:
118-
if ctx_filter is not None and context not in ctx_filter:
177+
stacklevel = 0
178+
179+
for path_def, checker in checkers:
180+
if not path_def:
119181
continue
182+
paths = [path_def] if isinstance(path_def, str) else path_def
183+
value_lists = [_get_value(obj, p, context) for p in paths]
120184
try:
121-
checker(obj, context)
185+
for value_tuple in zip(*value_lists):
186+
issue = checker(*value_tuple)
187+
if issue:
188+
stacklevel = stacklevel or _get_stacklevel()
189+
warnings.warn(
190+
f"{path_def}: {issue}",
191+
UserWarning,
192+
stacklevel=stacklevel,
193+
)
122194
except Exception:
123-
# Don't let a recommender break construction
124195
pass
196+

0 commit comments

Comments
 (0)