1616 go.Figure(data=[go.Scatter(y=[1, 2, 3])]) # may emit recommendation warnings
1717"""
1818
19+ from functools import partial
20+ import inspect
1921import os
2022import 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
78141def _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