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

Expanded range info shown in HTML repr #821

Merged
merged 18 commits into from
Sep 23, 2023
Merged
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
70 changes: 49 additions & 21 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import operator
import typing
import warnings
import html

# Allow this file to be used standalone if desired, albeit without JSON serialization
try:
Expand Down Expand Up @@ -3573,39 +3574,65 @@ def _name_if_set(parameterized):
return '' if default_name else parameterized.name


def _get_param_repr(key, val, p, truncate=40):
def truncate(str_, maxlen = 30):
"""Return HTML-safe truncated version of given string"""
rep = (str_[:(maxlen-2)] + '..') if (len(str_) > (maxlen-2)) else str_
return html.escape(rep)


def _get_param_repr(key, val, p, vallen=30, doclen=40):
"""HTML representation for a single Parameter object and its value"""
if hasattr(val, "_repr_html_"):
try:
value = val._repr_html_(open=False)
except:
value = val._repr_html_()
else:
rep = repr(val)
value = (rep[:truncate] + '..') if len(rep) > truncate else rep
value = truncate(repr(val), vallen)

modes = []
if p.constant:
modes.append('constant')
if p.readonly:
modes.append('read-only')
if getattr(p, 'allow_None', False):
modes.append('nullable')
mode = ' | '.join(modes)
if hasattr(p, 'bounds'):
bounds = p.bounds
if p.bounds is None:
range_ = '(-∞,∞)'
elif hasattr(p,'inclusive_bounds'):
# Numeric bounds use ( and [ to indicate exclusive and inclusive
bl,bu = p.bounds
il,iu = p.inclusive_bounds
lb = ('[' if il else '(') + ('-∞' if bl is None else str(bl))
ub = ('∞' if bu is None else str(bu)) + (']' if iu else ')')
range_ = lb + ', ' + ub
else:
range_ = repr(p.bounds)
elif hasattr(p, 'objects') and p.objects:
bounds = ', '.join(list(map(repr, p.objects)))
range_ = ', '.join(list(map(repr, p.objects)))
elif hasattr(p, 'class_'):
if isinstance(p.class_, tuple):
jbednar marked this conversation as resolved.
Show resolved Hide resolved
range_ = ' | '.join(kls.__name__ for kls in p.class_)
else:
range_ = p.class_.__name__
elif hasattr(p, 'regex'):
range_ = '.*' if p.regex is None else str(p.regex)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it's super frequent to define a regex for a String Parameter and wouldn't expect all Param users to be knowledgeable about regular expressions. I'd suggest either not including '.*' or being more explicit with e.g. regex(.*).

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason to include it is only that when allow_None is True and there's no regex, the range is shown as | None, which I think is confusing, given that I read | as "or", so it's "???? or None" (i.e., "what or None??"). Can you think of a better way to convey "any string" than .*? '' would be accurate since the empty string is a regex that matches every string, and then it would show '' | None, if that would look better. Or, sure, regex(.*), if you think that's clearer.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I agree that most options here aren't very good. Not sure I love it but if there's no regex I'd also be okay with str | None if allow_None or str otherwise.

Copy link
Member

Choose a reason for hiding this comment

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

Actually str | None for no regex + allow_None and nothing at all for no regex and allow_None=False is my preference.

Copy link
Member Author

@jbednar jbednar Aug 28, 2023

Choose a reason for hiding this comment

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

(That's the same reason I went with (-Inf,Inf) for numbers; | None looked odd without it. If anyone has a solution that conveys "any valid input for that type, or None" more clearly, happy to use that instead!)

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm; looks like @philippjfr 's notes weren't updated on my screen until after I made the above note. I don't mind "str", but I'd like us to use the same approach for number parameters as for string, since it's precisely the same concept: any allowed string, or any allowed number. Instead of (-Inf, Inf), would a number be "num" in this approach, as in "num | None"?

Copy link
Member Author

@jbednar jbednar Aug 28, 2023

Choose a reason for hiding this comment

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

Actually, None is a different type, so I suppose we really ought to be putting it into the Type column! I.e. the type isn't really Integer, it's Integer | None! I think that will eliminate all the awkwardness on the range field from | None, so I'll change it to do that unless someone strongly objects. Seems like the obvious next step from Philipp's proposal.

Assuming we do that, then there is still a question whether to combine the type and range columns. Combining them makes it clear that the constraints apply to the non-None type, not None: Integer | None, Integer [0, Inf) | None, List[Integer] len(0,10) | None.

I can't quite decide which I prefer; any votes?

else:
bounds = ''
range_ = ''

if p.readonly:
range_ = ' '.join(['<i>read-only</i>', range_])
elif p.constant:
range_ = ' '.join(['<i>constant</i>', range_])

if getattr(p, 'allow_None', False):
range_ = ' | '.join(['None', range_])

tooltip = f' class="param-doc-tooltip" data-tooltip="{escape(p.doc.strip())}"' if p.doc else ''

doc = "" if p.doc is None else truncate(p.doc.strip(), doclen)

return (
f'<tr>'
f' <td><tt{tooltip}>{key}</tt></td>'
f' <td><p{tooltip}>{key}</p></td>'
f' <td style="max-width: 200px;">{value}</td>'
f' <td>{p.__class__.__name__}</td>'
f' <td>{value}</td>'
f' <td style="max-width: 300px;">{bounds}</td>'
f' <td>{mode}</td>'
f' <td style="max-width: 300px;">{range_}</td>'
f' <td style="max-width: 500px;"><p{tooltip}>{doc}</p></td>'
f'</tr>\n'
)

Expand Down Expand Up @@ -3652,8 +3679,9 @@ def _parameterized_repr_html(p, open):
}
"""
openstr = " open" if open else ""
contents = "".join(_get_param_repr(key, val, p.param.params(key))
for key, val in p.param.get_param_values())
param_values = p.param.values().items()
contents = "".join(_get_param_repr(key, val, p.param[key])
for key, val in param_values)
return (
f'<style>{tooltip_css}</style>\n'
f'<details {openstr}>\n'
Expand All @@ -3662,7 +3690,7 @@ def _parameterized_repr_html(p, open):
' </summary>\n'
' <div style="padding-left:10px; padding-bottom:5px;">\n'
' <table style="max-width:100%; border:1px solid #AAAAAA;">\n'
f' <tr><th>Name</th><th>Type</th><th>{value_field}</th><th>Bounds/Objects</th><th>Mode</th></tr>\n'
f' <tr><th>Name</th><th>{value_field}</th><th>Type</th><th>Range</th><th>Doc</th></tr>\n'
f'{contents}\n'
' </table>\n </div>\n</details>\n'
)
Expand Down
11 changes: 11 additions & 0 deletions tests/testreprhtml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import param

from param.parameterized import _parameterized_repr_html


def test_repr_html_ClassSelector_tuple():
class P(param.Parameterized):
c = param.ClassSelector(class_=(str, int))

rhtml = _parameterized_repr_html(P, True)
assert 'None | str | int' in rhtml