diff --git a/param/parameterized.py b/param/parameterized.py index 398deb2cf..3622cfbc2 100644 --- a/param/parameterized.py +++ b/param/parameterized.py @@ -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: @@ -3612,45 +3613,62 @@ def type_script_repr(type_,imports,prefix,settings): dbprint_prefix=None -def _name_if_set(parameterized): - """Return the name of this Parameterized if explicitly set to other than the default""" - class_name = parameterized.__class__.__name__ - default_name = re.match('^'+class_name+'[0-9]+$', parameterized.name) - return '' if default_name else parameterized.name +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, truncate=40): +def _get_param_repr(key, val, p, vallen=30, doclen=40): """HTML representation for a single Parameter object and its value""" if isinstance(val, Parameterized) or (type(val) is type and issubclass(val, Parameterized)): value = val.param._repr_html_(open=False) elif hasattr(val, "_repr_html_"): 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 bl is None else ('>=' if il else '>') + str(bl) + ub = '' if bu is None else ('<=' if iu else '<') + str(bu) + range_ = lb + (', ' if lb and bu else '') + 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): + range_ = ' | '.join(kls.__name__ for kls in p.class_) + else: + range_ = p.class_.__name__ + elif hasattr(p, 'regex') and p.regex is not None: + range_ = f'regex({p.regex})' else: - bounds = '' + range_ = '' + + if p.readonly: + range_ = ' '.join(s for s in ['read-only', range_] if s) + elif p.constant: + range_ = ' '.join(s for s in ['constant', range_] if s) + + if getattr(p, 'allow_None', False): + range_ = ' '.join(s for s in ['nullable', range_] if s) + tooltip = f' class="param-doc-tooltip" data-tooltip="{escape(p.doc.strip())}"' if p.doc else '' + return ( f'' - f' {key}' - f' {p.__class__.__name__}' - f' {value}' - f' {bounds}' - f' {mode}' + f'

{key}

' + f' {value}' + f' {p.__class__.__name__}' + f' {range_}' f'\n' ) @@ -3659,7 +3677,7 @@ def _parameterized_repr_html(p, open): """HTML representation for a Parameterized object""" if isinstance(p, Parameterized): cls = p.__class__ - title = cls.name + "() " + _name_if_set(p) + title = cls.name + "()" value_field = 'Value' else: cls = p @@ -3669,12 +3687,12 @@ def _parameterized_repr_html(p, open): tooltip_css = """ .param-doc-tooltip{ position: relative; + cursor: help; } .param-doc-tooltip:hover:after{ content: attr(data-tooltip); background-color: black; color: #fff; - text-align: center; border-radius: 3px; padding: 10px; position: absolute; @@ -3682,8 +3700,7 @@ def _parameterized_repr_html(p, open): top: -5px; left: 100%; margin-left: 10px; - min-width: 100px; - min-width: 150px; + min-width: 250px; } .param-doc-tooltip:hover:before { content: ""; @@ -3708,7 +3725,7 @@ def _parameterized_repr_html(p, open): ' \n' '
\n' ' \n' - f' \n' + f' \n' f'{contents}\n' '
NameType{value_field}Bounds/ObjectsMode
Name{value_field}TypeRange
\n
\n\n' ) diff --git a/tests/testparameterizedrepr.py b/tests/testparameterizedrepr.py index 470914c94..c5c01b528 100644 --- a/tests/testparameterizedrepr.py +++ b/tests/testparameterizedrepr.py @@ -252,48 +252,3 @@ class T(P): pass ) assert t.param.pprint() == 'T()' - - -# HTML repr - -class TestHTMLRepr: - - @pytest.fixture - def Sub(self): - class Sub(param.Parameterized): - xsub = param.Parameter() - - return Sub - - @pytest.fixture - def P(self, Sub): - class P(param.Parameterized): - constant = param.Parameter(constant=True) - readonly = param.Parameter(readonly=True) - allow_None = param.Parameter(1, allow_None=False) - bounds = param.Number(bounds=(-10, 10)) - objects_list = param.Selector(objects=[1, 2]) - objects_dict = param.Selector(objects=dict(a=1, b=2)) - sub = param.ClassSelector(default=Sub(xsub=1), class_=Sub) - - return P - - def test_repr_in_place(self): - assert hasattr(param.Parameterized.param, '_repr_html_') - assert hasattr(param.Parameterized().param, '_repr_html_') - - def test_html_repr_class_str(self, P): - html = P.param._repr_html_() - assert isinstance(html, str) - - def test_html_repr_inst_str(self, P): - html = P().param._repr_html_() - assert isinstance(html, str) - - def test_html_repr_class_sub(self, P): - html = P.param._repr_html_() - assert '
\n \n Sub' in html - - def test_html_repr_inst_sub(self, P): - html = P().param._repr_html_() - assert '
\n \n Sub' in html diff --git a/tests/testreprhtml.py b/tests/testreprhtml.py new file mode 100644 index 000000000..d989eb0a1 --- /dev/null +++ b/tests/testreprhtml.py @@ -0,0 +1,62 @@ +import param +import pytest + +from param.parameterized import _parameterized_repr_html + + +class TestHTMLRepr: + + @pytest.fixture + def Sub(self): + class Sub(param.Parameterized): + xsub = param.Parameter() + + return Sub + + @pytest.fixture + def P(self, Sub): + class P(param.Parameterized): + constant = param.Parameter(constant=True) + readonly = param.Parameter(readonly=True) + allow_None = param.Parameter(1, allow_None=False) + bounds = param.Number(bounds=(-10, 10)) + objects_list = param.Selector(objects=[1, 2]) + objects_dict = param.Selector(objects=dict(a=1, b=2)) + sub = param.ClassSelector(default=Sub(xsub=1), class_=Sub) + + return P + + def test_repr_in_place(self): + assert hasattr(param.Parameterized.param, '_repr_html_') + assert hasattr(param.Parameterized().param, '_repr_html_') + + def test_html_repr_class_str(self, P): + html = P.param._repr_html_() + assert isinstance(html, str) + + def test_html_repr_inst_str(self, P): + html = P().param._repr_html_() + assert isinstance(html, str) + + def test_html_repr_class_sub(self, P): + html = P.param._repr_html_() + assert '
\n \n Sub' in html + + def test_html_repr_inst_sub(self, P): + html = P().param._repr_html_() + assert '
\n \n Sub' in html + + def test_html_repr_ClassSelector_tuple(self): + class P(param.Parameterized): + c = param.ClassSelector(class_=(str, int)) + + rhtml = _parameterized_repr_html(P, True) + assert 'str | int' in rhtml + + def test_html_repr_title_class(self, P): + html = P.param._repr_html_() + assert 'P' in html + + def test_html_repr_title_instance(self, P): + html = P().param._repr_html_() + assert 'P()' in html