Skip to content

Commit

Permalink
Added HTML repr for Parameterized (#425)
Browse files Browse the repository at this point in the history
Co-authored-by: Philipp Rudiger <prudiger@anaconda.com>
Co-authored-by: maximlt <mliquet@anaconda.com>
  • Loading branch information
3 people authored Jun 22, 2023
1 parent 76086ef commit 72a52c7
Show file tree
Hide file tree
Showing 3 changed files with 205 additions and 44 deletions.
55 changes: 47 additions & 8 deletions examples/user_guide/Parameters.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -442,12 +442,22 @@
"\n",
"Parameter inheritance like this lets you (a) use a parameter in many subclasses without having to define it more than once, and (b) control the value of that parameter conveniently across the entire set of subclasses and instances, as long as that attribute has not been set on those objects already. Using inheritance in this way is a very convenient mechanism for setting default values and other \"global\" parameters, whether before a program starts executing or during it.\n",
"\n",
"`help(b)` or `help(B)` will list all parameters:"
"`help(b)` or `help(B)` will list all parameters. You can also prefix or suffix a Parameterized object with `?` in an IPython console/Notebook to display the help:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dd711290-b6f0-4e9e-bbce-c400da27a3c2",
"metadata": {},
"outputs": [],
"source": [
"B?"
]
},
{
"cell_type": "markdown",
"id": "16ad7973",
"id": "54ecbc24-14eb-4c07-8a00-0fb79b6241da",
"metadata": {},
"source": [
"<img src=\"../assets/param_help.png\" alt=\"Param help\"></img>"
Expand Down Expand Up @@ -781,10 +791,11 @@
"source": [
"## Displaying Parameterized objects\n",
"\n",
"Most of the important behavior of Parameterized is to do with instantiation, getting, and setting, as described above. Parameterized also provides a few public methods for creating a string representation of the Parameterized object and its parameters:\n",
"Most of the important behavior of Parameterized is to do with instantiation, getting, and setting, as described above. Parameterized also provides a few public methods for creating string representations of the Parameterized object and its parameters:\n",
"\n",
"- `Parameterized.__str__()`: A concise, non-executable representation of the name and class of this object\n",
"- `Parameterized.__repr__()`: A representation of this object and its parameter values as if it were Python code calling the constructor (`classname(parameter1=x,parameter2=y,...)`)\n",
"- `Parameterized._repr_html_()` and `Parameterize.param._repr_html_()`: A rich HTML representation of the object with its parameters listed in a table together with their metadata.\n",
"- `Parameterized.param.pprint()`: Customizable, hierarchical pretty-printed representation of this Parameterized and (recursively) any of its parameters that are Parameterized objects. See [Serialization and Persistence](Serialization_and_Persistence.ipynb) for details on customizing `pprint`."
]
},
Expand All @@ -798,13 +809,13 @@
"import param\n",
"\n",
"class Q(param.Parameterized):\n",
" a = param.Number(default=39, bounds=(0,50))\n",
" b = param.String(default=\"str\")\n",
" a = param.Number(default=39, bounds=(0,50), doc='Number a')\n",
" b = param.String(default=\"str\", doc='A string')\n",
"\n",
"class P(Q):\n",
" c = param.ClassSelector(default=Q(), class_=Q)\n",
" e = param.ClassSelector(default=param.Parameterized(), class_=param.Parameterized)\n",
" f = param.Range(default=(0,1))\n",
" c = param.ClassSelector(default=Q(), class_=Q, doc='An instance of Q')\n",
" e = param.ClassSelector(default=param.Parameterized(), class_=param.Parameterized, doc='A Parameterized instance')\n",
" f = param.Range(default=(0,1), doc='A range')\n",
"\n",
"p = P(f=(2,3), c=P(f=(42,43)), name=\"demo\")"
]
Expand All @@ -829,6 +840,34 @@
"p.__repr__()"
]
},
{
"cell_type": "markdown",
"id": "799c1eeb-71c2-40a3-ad06-fd3ed8eda501",
"metadata": {},
"source": [
"The HTML representation of a `Parameterized` instance is automatically displayed in a Notebook. If the object you are displaying has overriden the `Parameterized._repr_html()` method to implement its own rich display - which is for instance the case for Panel components - call `.param` on your object to see its Param rich display."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4c17a53b-3acc-4e6e-ab93-e5d09728e1c5",
"metadata": {},
"outputs": [],
"source": [
"p # or p.param"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "64628d11-7f2c-4718-bd8a-7a381e282047",
"metadata": {},
"outputs": [],
"source": [
"P # or P.param"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand Down
178 changes: 142 additions & 36 deletions param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
except ImportError:
serializer = None


from collections import defaultdict, namedtuple, OrderedDict
from functools import partial, wraps, reduce
from operator import itemgetter,attrgetter
from html import escape
from operator import itemgetter, attrgetter
from threading import get_ident
from types import FunctionType, MethodType

Expand Down Expand Up @@ -324,29 +324,24 @@ def wrapper(cls):
return wrapper



class bothmethod: # pylint: disable-msg=R0903
class bothmethod:
"""
'optional @classmethod'
A decorator that allows a method to receive either the class
object (if called on the class) or the instance object
(if called on the instance) as its first argument.
Code (but not documentation) copied from:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/523033.
"""
# pylint: disable-msg=R0903

def __init__(self, func):
self.func = func
def __init__(self, method):
self.method = method

# i.e. this is also a non-data descriptor
def __get__(self, obj, type_=None):
if obj is None:
return wraps(self.func)(partial(self.func, type_))
def __get__(self, instance, owner):
if instance is None:
# Class call
return self.method.__get__(owner)
else:
return wraps(self.func)(partial(self.func, obj))
# Instance call
return self.method.__get__(instance, owner)


def _getattrr(obj, attr, *args):
Expand Down Expand Up @@ -411,6 +406,29 @@ def get_method_owner(method):
return method.__self__


def recursive_repr(fillvalue='...'):
'Decorator to make a repr function return fillvalue for a recursive call'
# Copy of Python 3.2 reprlib's recursive_repr but allowing extra arguments

def decorating_function(user_function):
repr_running = set()

@wraps(user_function)
def wrapper(self, *args, **kwargs):
key = id(self), get_ident()
if key in repr_running:
return fillvalue
repr_running.add(key)
try:
result = user_function(self, *args, **kwargs)
finally:
repr_running.discard(key)
return result
return wrapper

return decorating_function


@accept_arguments
def depends(func, *dependencies, watch=False, on_init=False, **kw):
"""Annotates a function or Parameterized method to express its dependencies.
Expand Down Expand Up @@ -1948,6 +1966,10 @@ def _watch_group(self_, obj, name, queued, group, attribute=None):
return dep_obj.param._watch(
mcaller, params, param_dep.what, queued=queued, precedence=-1)

@recursive_repr()
def _repr_html_(self_, open=True):
return _parameterized_repr_html(self_.self_or_cls, open)

# Classmethods

# PARAM3_DEPRECATION
Expand Down Expand Up @@ -3300,9 +3322,9 @@ def type_script_repr(type_,imports,prefix,settings):
imports.append('import %s'%module)
return module+'.'+type_.__name__

script_repr_reg[list]=container_script_repr
script_repr_reg[tuple]=container_script_repr
script_repr_reg[FunctionType]=function_script_repr
script_repr_reg[list] = container_script_repr
script_repr_reg[tuple] = container_script_repr
script_repr_reg[FunctionType] = function_script_repr


#: If not None, the value of this Parameter will be called (using '()')
Expand All @@ -3312,26 +3334,106 @@ def type_script_repr(type_,imports,prefix,settings):
dbprint_prefix=None


# Copy of Python 3.2 reprlib's recursive_repr but allowing extra arguments
def recursive_repr(fillvalue='...'):
'Decorator to make a repr function return fillvalue for a recursive call'
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 decorating_function(user_function):
repr_running = set()

def wrapper(self, *args, **kwargs):
key = id(self), get_ident()
if key in repr_running:
return fillvalue
repr_running.add(key)
try:
result = user_function(self, *args, **kwargs)
finally:
repr_running.discard(key)
return result
return wrapper
def _get_param_repr(key, val, p, truncate=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

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
elif hasattr(p, 'objects') and p.objects:
bounds = ', '.join(list(map(repr, p.objects)))
else:
bounds = ''
tooltip = f' class="param-doc-tooltip" data-tooltip="{escape(p.doc.strip())}"' if p.doc else ''
return (
f'<tr>'
f' <td><tt{tooltip}>{key}</tt></td>'
f' <td>{p.__class__.__name__}</td>'
f' <td>{value}</td>'
f' <td style="max-width: 300px;">{bounds}</td>'
f' <td>{mode}</td>'
f'</tr>\n'
)

return decorating_function

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)
value_field = 'Value'
else:
cls = p
title = cls.name
value_field = 'Default'

tooltip_css = """
.param-doc-tooltip{
position: relative;
}
.param-doc-tooltip:hover:after{
content: attr(data-tooltip);
background-color: black;
color: #fff;
text-align: center;
border-radius: 3px;
padding: 10px;
position: absolute;
z-index: 1;
top: -5px;
left: 100%;
margin-left: 10px;
min-width: 100px;
min-width: 150px;
}
.param-doc-tooltip:hover:before {
content: "";
position: absolute;
top: 50%;
left: 100%;
margin-top: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent black transparent transparent;
}
"""
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())
return (
f'<style>{tooltip_css}</style>\n'
f'<details {openstr}>\n'
' <summary style="display:list-item; outline:none;">\n'
f' <tt>{title}</tt>\n'
' </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'{contents}\n'
' </table>\n </div>\n</details>\n'
)


class Parameterized(metaclass=ParameterizedMetaclass):
Expand Down Expand Up @@ -3651,6 +3753,10 @@ def _state_pop(self):
elif hasattr(g,'state_pop') and isinstance(g,Parameterized):
g.state_pop()

@bothmethod
@recursive_repr()
def _repr_html_(self_or_cls, open=True):
return _parameterized_repr_html(self_or_cls, open)


def print_all_param_defaults():
Expand Down
16 changes: 16 additions & 0 deletions tests/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from param import guess_param_types, resolve_path
from param.parameterized import bothmethod

try:
import numpy as np
Expand Down Expand Up @@ -319,3 +320,18 @@ def test_resolve_path_search_paths_multiple_file(tmpdir):
assert os.path.basename(p) == 'foo2'
assert os.path.isabs(p)
assert p == fp2


def test_both_method():

class A:

@bothmethod
def method(self_or_cls):
return self_or_cls

assert A.method() is A

a = A()

assert a.method() is a

0 comments on commit 72a52c7

Please sign in to comment.