Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deep magic underscore error messages rebase #2843

Merged
merged 26 commits into from
Nov 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2209b2d
Better magic underscore errors
nicholas-esterer Oct 19, 2020
afd5b7a
Remove whitespace-only changes
nicholas-esterer Oct 20, 2020
b894b89
Assert that errors are raised
nicholas-esterer Oct 20, 2020
4c659db
Also state when a property doesn't support subscripting
nicholas-esterer Oct 20, 2020
2bf2526
Tests for non-subscriptable property errors
nicholas-esterer Oct 20, 2020
5695969
updated changelog
nicholas-esterer Oct 20, 2020
22b3e03
Improved error message when subscripting types that don't support it
nicholas-esterer Oct 23, 2020
c09c03e
Removed garbage line from test_dict_path_errors.py
nicholas-esterer Oct 23, 2020
f4be2b1
Changed PlotlyKeyError's superclass to KeyError
nicholas-esterer Nov 4, 2020
73051a6
BasePlotlyType._raise_on_invalid_property_error raises PlotlyKeyError
nicholas-esterer Nov 4, 2020
651b712
Cast some errors to PlotlyKeyError
nicholas-esterer Nov 4, 2020
9bb2470
Updated the tests to reflect the new Exception behaviour
nicholas-esterer Nov 4, 2020
bf4cd97
BasePlotlyType.__setitem__ exceptions casted to ValueError
nicholas-esterer Nov 5, 2020
7d42ffe
Merge master's whitespace changes
nicholas-esterer Nov 5, 2020
a18b341
Merge branch 'master' into deep-magic-underscore-error-msg-rebase
nicholas-esterer Nov 5, 2020
4bda7b2
Now subscripting errors triggered on types throwing TypeError
nicholas-esterer Nov 5, 2020
9e1b667
subscripting error tests compatible with Python2
nicholas-esterer Nov 5, 2020
d6aee64
Changed dict path error display to always ^
nicholas-esterer Nov 9, 2020
c6e5b4d
Try taking length of string-like objects
nicholas-esterer Nov 9, 2020
82f9bb1
leading, trailing, multiple underscores detected in dict path strings
nicholas-esterer Nov 9, 2020
70f18ca
Added tests for leading, trailing and embedded extra underscore errors
nicholas-esterer Nov 10, 2020
00851fa
Complain about trailing underscores, find closest key
nicholas-esterer Nov 10, 2020
d2bc400
Updated error messages for trailing underscores and find closest key
nicholas-esterer Nov 10, 2020
20518c1
Key guessing before and after list of valid properties
nicholas-esterer Nov 10, 2020
4066ae2
Test single property key guessing
nicholas-esterer Nov 11, 2020
86f4217
Merge branch 'master' into deep-magic-underscore-error-msg-rebase
nicholas-esterer Nov 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- The `add_trace`, `add_shape`, `add_annotation`, `add_layout_image`, `add_hline`, `add_vline`, `add_hrect`, `add_vrect` functions accept an argument `exclude_empty_subplots` which if `True`, only adds the object to subplots already containing traces or layout objects. This is useful in conjunction with the `row="all"` and `col="all"` arguments. ([#2840](https://github.com/plotly/plotly.py/pull/2840))
- For all `go.Figure` functions accepting a selector argument (e.g., `select_traces`), this argument can now also be a function which is passed each relevant graph object (in the case of `select_traces`, it is passed every trace in the figure). For graph objects where this function returns true, the graph object is included in the selection. ([#2844](https://github.com/plotly/plotly.py/pull/2844))

### Added
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move this to the 4.13 block


- Better magic underscore error messages. For example, `some_fig.update_layout(geo_ltaxis_showgrid=True)` shows `Bad property path:\ngeo_ltaxis_showgrid\n ^` and lists the valid properties for `geo`.

### Updated

- Updated Plotly.js to version 1.57.1. See the [plotly.js CHANGELOG](https://github.com/plotly/plotly.js/blob/v1.57.1/CHANGELOG.md) for more information. These changes are reflected in the auto-generated `plotly.graph_objects` module.
Expand Down
12 changes: 12 additions & 0 deletions packages/python/plotly/_plotly_utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,15 @@ def __init__(self, obj, path, notes=()):
super(PlotlyDataTypeError, self).__init__(
message=message, path=path, notes=notes
)


class PlotlyKeyError(KeyError):
"""
KeyErrors are not printed as beautifully as other errors (this is so that
{}[''] prints "KeyError: ''" and not "KeyError:"). So here we use
LookupError's __str__ to make a PlotlyKeyError object which will print nicer
error messages for KeyErrors.
"""

def __str__(self):
return LookupError.__str__(self)
182 changes: 182 additions & 0 deletions packages/python/plotly/_plotly_utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json as _json
import sys
import re
from functools import reduce

from _plotly_utils.optional_imports import get_module
from _plotly_utils.basevalidators import ImageUriValidator
Expand All @@ -10,6 +11,20 @@
PY36_OR_LATER = sys.version_info >= (3, 6)


def cumsum(x):
"""
Custom cumsum to avoid a numpy import.
"""

def _reducer(a, x):
if len(a) == 0:
return [x]
return a + [a[-1] + x]

ret = reduce(_reducer, x, [])
return ret


class PlotlyJSONEncoder(_json.JSONEncoder):
"""
Meant to be passed as the `cls` kwarg to json.dumps(obj, cls=..)
Expand Down Expand Up @@ -256,3 +271,170 @@ def _get_int_type():
else:
int_type = (int,)
return int_type


def split_multichar(ss, chars):
"""
Split all the strings in ss at any of the characters in chars.
Example:

>>> ss = ["a.string[0].with_separators"]
>>> chars = list(".[]_")
>>> split_multichar(ss, chars)
['a', 'string', '0', '', 'with', 'separators']

:param (list) ss: A list of strings.
:param (list) chars: Is a list of chars (note: not a string).
"""
if len(chars) == 0:
return ss
c = chars.pop()
ss = reduce(lambda x, y: x + y, map(lambda x: x.split(c), ss))
return split_multichar(ss, chars)


def split_string_positions(ss):
"""
Given a list of strings split using split_multichar, return a list of
integers representing the indices of the first character of every string in
the original string.
Example:

>>> ss = ["a.string[0].with_separators"]
>>> chars = list(".[]_")
>>> ss_split = split_multichar(ss, chars)
>>> ss_split
['a', 'string', '0', '', 'with', 'separators']
>>> split_string_positions(ss_split)
[0, 2, 9, 11, 12, 17]

:param (list) ss: A list of strings.
"""
return list(
map(
lambda t: t[0] + t[1],
zip(range(len(ss)), cumsum([0] + list(map(len, ss[:-1])))),
)
)


def display_string_positions(p, i=None, offset=0, length=1, char="^", trim=True):
"""
Return a string that is whitespace except at p[i] which is replaced with char.
If i is None then all the indices of the string in p are replaced with char.

Example:

>>> ss = ["a.string[0].with_separators"]
>>> chars = list(".[]_")
>>> ss_split = split_multichar(ss, chars)
>>> ss_split
['a', 'string', '0', '', 'with', 'separators']
>>> ss_pos = split_string_positions(ss_split)
>>> ss[0]
'a.string[0].with_separators'
>>> display_string_positions(ss_pos,4)
' ^'
>>> display_string_positions(ss_pos,4,offset=1,length=3,char="~",trim=False)
' ~~~ '
>>> display_string_positions(ss_pos)
'^ ^ ^ ^^ ^'
:param (list) p: A list of integers.
:param (integer|None) i: Optional index of p to display.
:param (integer) offset: Allows adding a number of spaces to the replacement.
:param (integer) length: Allows adding a replacement that is the char
repeated length times.
:param (str) char: allows customizing the replacement character.
:param (boolean) trim: trims the remaining whitespace if True.
"""
s = [" " for _ in range(max(p) + 1 + offset + length)]
maxaddr = 0
if i is None:
for p_ in p:
for l in range(length):
maxaddr = p_ + offset + l
s[maxaddr] = char
else:
for l in range(length):
maxaddr = p[i] + offset + l
s[maxaddr] = char
ret = "".join(s)
if trim:
ret = ret[: maxaddr + 1]
return ret


def chomp_empty_strings(strings, c, reverse=False):
"""
Given a list of strings, some of which are the empty string "", replace the
empty strings with c and combine them with the closest non-empty string on
the left or "" if it is the first string.
Examples:
for c="_"
['hey', '', 'why', '', '', 'whoa', '', ''] -> ['hey_', 'why__', 'whoa__']
['', 'hi', '', "I'm", 'bob', '', ''] -> ['_', 'hi_', "I'm", 'bob__']
['hi', "i'm", 'a', 'good', 'string'] -> ['hi', "i'm", 'a', 'good', 'string']
Some special cases are:
[] -> []
[''] -> ['']
['', ''] -> ['_']
['', '', '', ''] -> ['___']
If reverse is true, empty strings are combined with closest non-empty string
on the right or "" if it is the last string.
"""

def _rev(l):
return [s[::-1] for s in l][::-1]

if reverse:
return _rev(chomp_empty_strings(_rev(strings), c))
if not len(strings):
return strings
if sum(map(len, strings)) == 0:
return [c * (len(strings) - 1)]

class _Chomper:
def __init__(self, c):
self.c = c

def __call__(self, x, y):
# x is list up to now
# y is next item in list
# x should be [""] initially, and then empty strings filtered out at the
# end
if len(y) == 0:
return x[:-1] + [x[-1] + self.c]
else:
return x + [y]

return list(filter(len, reduce(_Chomper(c), strings, [""])))


# taken from
# https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python
def levenshtein(s1, s2):
if len(s1) < len(s2):
return levenshtein(s2, s1) # len(s1) >= len(s2)
if len(s2) == 0:
return len(s1)
previous_row = range(len(s2) + 1)
for i, c1 in enumerate(s1):
current_row = [i + 1]
for j, c2 in enumerate(s2):
# j+1 instead of j since previous_row and current_row are one character longer
# than s2
insertions = previous_row[j + 1] + 1
deletions = current_row[j] + 1
substitutions = previous_row[j] + (c1 != c2)
current_row.append(min(insertions, deletions, substitutions))
previous_row = current_row
return previous_row[-1]


def find_closest_string(string, strings):
def _key(s):
# sort by levenshtein distance and lexographically to maintain a stable
# sort for different keys with the same levenshtein distance
return (levenshtein(s, string), s)

return sorted(strings, key=_key)[0]
Loading