Skip to content

Deep magic underscore error messages #2824

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

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ node_modules/

# virtual envs
vv
venv
venv*

# dist files
build
Expand Down
75 changes: 75 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,8 @@
import json as _json
import sys
import re
from functools import reduce
from numpy import cumsum
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure we want to import numpy here ... it's not a hard dependency of plotly

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, numpy shouldn't be a hard dependency here. Here's an alternative approach you can use for cumsum if you need it (https://realpython.com/python-reduce-function/#comparing-reduce-and-accumulate).


from _plotly_utils.optional_imports import get_module
from _plotly_utils.basevalidators import ImageUriValidator
Expand Down Expand Up @@ -256,3 +258,76 @@ 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):
"""
Return a string that is whitespace except at p[i] which is replaced with ^.
If i is None then all the indices of the string in p are replaced with ^.
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)
' ^'
:param (list) p: A list of integers.
:param (integer|None) i: Optional index of p to display.
"""
s = [" " for _ in range(max(p) + 1)]
if i is None:
for p_ in p:
s[p_] = "^"
else:
s[p[i]] = "^"
return "".join(s)
140 changes: 92 additions & 48 deletions packages/python/plotly/plotly/basedatatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from copy import deepcopy, copy

from _plotly_utils.utils import _natural_sort_strings, _get_int_type
import _plotly_utils
from .optional_imports import get_module

# Create Undefined sentinel value
Expand All @@ -18,6 +19,41 @@
Undefined = object()


def _make_prop_err_msg(path, err_pos):
"""
Returns a string like
path.to[1].some_property
^
where "some" is the first bad property
here path would be "path_to_some_property" and err_pos would be 8
"""
s = "%s\n" % (path,)
s += "".join(" " for _ in len(err_pos))
s += "^\n"
return s


def _walk_prop_tree(obj, prop, path_string):
"""
obj is the object in which the first property is looked up
prop is a tuple of property names as returned by BaseFigure._str_to_dict_path
returns a tuple
- the first item is a boolean which is True if the property
described by the prop tuple is in self or False otherwise
- the second item is an Exception object if the property name
lookup failed containing the path up to where the property name
lookup failed, otherwise None. Callers can choose whether or not
to raise this exception, depending on the context.
"""
valid_path = ""
while len(prop):
p = prop[0]
try:
obj = obj[p]
except KeyError as e:
pass


class BaseFigure(object):
"""
Base class for all figure types (both widget and non-widget)
Expand Down Expand Up @@ -1334,10 +1370,63 @@ def _normalize_trace_indexes(self, trace_indexes):
trace_indexes = [trace_indexes]
return list(trace_indexes)

@staticmethod
def _str_to_dict_path_full(key_path_str):
"""
Convert a key path string into a tuple of key path elements and also
return a tuple of indices marking the beginning of each element in the
string.

Parameters
----------
key_path_str : str
Key path string, where nested keys are joined on '.' characters
and array indexes are specified using brackets
(e.g. 'foo.bar[1]')
Returns
-------
tuple[str | int]
tuple [int]
"""
key_path2 = _plotly_utils.utils.split_multichar([key_path_str], list(".[]"))
# Split out underscore
# e.g. ['foo', 'bar_baz', '1'] -> ['foo', 'bar', 'baz', '1']
key_path3 = []
underscore_props = BaseFigure._valid_underscore_properties

def _make_hyphen_key(key):
if "_" in key[1:]:
# For valid properties that contain underscores (error_x)
# replace the underscores with hyphens to protect them
# from being split up
for under_prop, hyphen_prop in underscore_props.items():
key = key.replace(under_prop, hyphen_prop)
return key

def _make_underscore_key(key):
return key.replace("-", "_")

key_path2b = map(_make_hyphen_key, key_path2)
key_path2c = _plotly_utils.utils.split_multichar(key_path2b, list("_"))
key_path2d = list(map(_make_underscore_key, key_path2c))
elem_idcs = tuple(_plotly_utils.utils.split_string_positions(list(key_path2d)))
# remove empty strings
key_path3 = list(filter(len, key_path2d))

# Convert elements to ints if possible.
# e.g. ['foo', 'bar', '0'] -> ['foo', 'bar', 0]
for i in range(len(key_path3)):
try:
key_path3[i] = int(key_path3[i])
except ValueError as _:
pass

return (tuple(key_path3), elem_idcs)

@staticmethod
def _str_to_dict_path(key_path_str):
"""
Convert a key path string into a tuple of key path elements
Convert a key path string into a tuple of key path elements.

Parameters
----------
Expand All @@ -1361,53 +1450,8 @@ def _str_to_dict_path(key_path_str):
# Nothing to do
return key_path_str
else:
# Split string on periods.
# e.g. 'foo.bar_baz[1]' -> ['foo', 'bar_baz[1]']
key_path = key_path_str.split(".")

# Split out bracket indexes.
# e.g. ['foo', 'bar_baz[1]'] -> ['foo', 'bar_baz', '1']
key_path2 = []
for key in key_path:
match = BaseFigure._bracket_re.match(key)
if match:
key_path2.extend(match.groups())
else:
key_path2.append(key)

# Split out underscore
# e.g. ['foo', 'bar_baz', '1'] -> ['foo', 'bar', 'baz', '1']
key_path3 = []
underscore_props = BaseFigure._valid_underscore_properties
for key in key_path2:
if "_" in key[1:]:
# For valid properties that contain underscores (error_x)
# replace the underscores with hyphens to protect them
# from being split up
for under_prop, hyphen_prop in underscore_props.items():
key = key.replace(under_prop, hyphen_prop)

# Split key on underscores
key = key.split("_")

# Replace hyphens with underscores to restore properties
# that include underscores
for i in range(len(key)):
key[i] = key[i].replace("-", "_")

key_path3.extend(key)
else:
key_path3.append(key)

# Convert elements to ints if possible.
# e.g. ['foo', 'bar', '0'] -> ['foo', 'bar', 0]
for i in range(len(key_path3)):
try:
key_path3[i] = int(key_path3[i])
except ValueError as _:
pass

return tuple(key_path3)
ret = BaseFigure._str_to_dict_path_full(key_path_str)[0]
return ret

@staticmethod
def _set_in(d, key_path_str, v):
Expand Down
Loading