Skip to content

Commit 00851fa

Browse files
Complain about trailing underscores, find closest key
Tests in tests/test_core/test_errors/test_dict_path_errors.py not yet updated though.
1 parent 70f18ca commit 00851fa

File tree

3 files changed

+70
-9
lines changed

3 files changed

+70
-9
lines changed

packages/python/plotly/_plotly_utils/utils.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,10 +364,10 @@ def display_string_positions(p, i=None, offset=0, length=1, char="^", trim=True)
364364
return ret
365365

366366

367-
def chomp_empty_strings(strings, c):
367+
def chomp_empty_strings(strings, c, reverse=False):
368368
"""
369369
Given a list of strings, some of which are the empty string "", replace the
370-
empty strings with "_" and combine them with the closest non-empty string on
370+
empty strings with c and combine them with the closest non-empty string on
371371
the left or "" if it is the first string.
372372
Examples:
373373
for c="_"
@@ -379,7 +379,15 @@ def chomp_empty_strings(strings, c):
379379
[''] -> ['']
380380
['', ''] -> ['_']
381381
['', '', '', ''] -> ['___']
382+
If reverse is true, empty strings are combined with closest non-empty string
383+
on the right or "" if it is the last string.
382384
"""
385+
386+
def _rev(l):
387+
return [s[::-1] for s in l][::-1]
388+
389+
if reverse:
390+
return _rev(chomp_empty_strings(_rev(strings), c))
383391
if not len(strings):
384392
return strings
385393
if sum(map(len, strings)) == 0:
@@ -400,3 +408,31 @@ def __call__(self, x, y):
400408
return x + [y]
401409

402410
return list(filter(len, reduce(_Chomper(c), strings, [""])))
411+
412+
413+
# taken from
414+
# https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python
415+
def levenshtein(s1, s2):
416+
if len(s1) < len(s2):
417+
return levenshtein(s2, s1) # len(s1) >= len(s2)
418+
if len(s2) == 0:
419+
return len(s1)
420+
previous_row = range(len(s2) + 1)
421+
for i, c1 in enumerate(s1):
422+
current_row = [i + 1]
423+
for j, c2 in enumerate(s2):
424+
# j+1 instead of j since previous_row and current_row are one character longer
425+
# than s2
426+
insertions = previous_row[j + 1] + 1
427+
deletions = current_row[j] + 1
428+
substitutions = previous_row[j] + (c1 != c2)
429+
current_row.append(min(insertions, deletions, substitutions))
430+
previous_row = current_row
431+
return previous_row[-1]
432+
433+
434+
def find_closest_string(string, strings):
435+
def _key(s):
436+
return levenshtein(s, string)
437+
438+
return sorted(strings, key=_key)[0]

packages/python/plotly/plotly/basedatatypes.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
split_string_positions,
1919
display_string_positions,
2020
chomp_empty_strings,
21+
find_closest_string,
2122
)
2223
from _plotly_utils.exceptions import PlotlyKeyError
2324
from .optional_imports import get_module
@@ -95,9 +96,9 @@ def _split_and_chomp(s):
9596
return s
9697
s_split = split_multichar([s], list("_"))
9798
# handle key paths like "a_path_", "_another_path", or
98-
# "yet__another_path" by joining extra "_" to the string to the left or
99-
# the empty string if at the beginning
100-
s_chomped = chomp_empty_strings(s_split, "_")
99+
# "yet__another_path" by joining extra "_" to the string to the right or
100+
# the empty string if at the end
101+
s_chomped = chomp_empty_strings(s_split, "_", reverse=True)
101102
return s_chomped
102103

103104
# after running _split_and_chomp on key_path2b, it will be a list
@@ -204,14 +205,25 @@ def _check_path_in_prop_tree(obj, path, error_cast=None):
204205
# In case i is 0, the best we can do is indicate the first
205206
# property in the string as having caused the error
206207
disp_i = max(i - 1, 0)
208+
dict_item_len = _len_dict_item(prop[disp_i])
209+
# if the path has trailing underscores, the prop string will start with "_"
210+
trailing_underscores = ""
211+
if prop[i][0] == "_":
212+
trailing_underscores = " and path has trailing underscores"
213+
# if the path has trailing underscores and the display index is
214+
# one less than the prop index (see above), then we can also
215+
# indicate the offending underscores
216+
if (trailing_underscores != "") and (disp_i != i):
217+
dict_item_len += _len_dict_item(prop[i])
207218
arg += """
208219
209-
Property does not support subscripting:
220+
Property does not support subscripting%s:
210221
%s
211222
%s""" % (
223+
trailing_underscores,
212224
path,
213225
display_string_positions(
214-
prop_idcs, disp_i, length=_len_dict_item(prop[disp_i]), char="^"
226+
prop_idcs, disp_i, length=dict_item_len, char="^"
215227
),
216228
)
217229
else:
@@ -226,6 +238,17 @@ def _check_path_in_prop_tree(obj, path, error_cast=None):
226238
prop_idcs, i, length=_len_dict_item(prop[i]), char="^"
227239
),
228240
)
241+
guessed_prop = None
242+
# If obj has _valid_props then we can try and guess what key was intended
243+
try:
244+
guessed_prop = find_closest_string(prop[i], obj._valid_props)
245+
except Exception:
246+
pass
247+
if guessed_prop is not None:
248+
arg += """
249+
Did you mean "%s"?""" % (
250+
guessed_prop,
251+
)
229252
# Make KeyError more pretty by changing it to a PlotlyKeyError,
230253
# because the Python interpreter has a special way of printing
231254
# KeyError

packages/python/plotly/plotly/tests/test_core/test_errors/test_dict_path_errors.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def test_raises_on_bad_dot_property(some_fig):
4949
e.args[0].find(
5050
"""Bad property path:
5151
layout.shapes[1].x2000
52-
^^^^^"""
52+
^^^^^
53+
Did you mean "x0"?"""
5354
)
5455
>= 0
5556
)
@@ -68,7 +69,8 @@ def test_raises_on_bad_ancestor_dot_property(some_fig):
6869
e.args[0].find(
6970
"""Bad property path:
7071
layout.shapa[1].x2000
71-
^^^^^"""
72+
^^^^^
73+
Did you mean "shapes"?"""
7274
)
7375
>= 0
7476
)

0 commit comments

Comments
 (0)