Skip to content

Commit

Permalink
Add html repr (#883)
Browse files Browse the repository at this point in the history
  • Loading branch information
edeno committed Jun 29, 2023
1 parent 2f9ec56 commit 6043e77
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 15 deletions.
97 changes: 97 additions & 0 deletions src/hdmf/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,103 @@ def __repr__(self):
template += " {}: {}\n".format(k, v)
return template

def _repr_html_(self):
CSS_STYLE = """
<style>
.container-fields {
font-family: "Open Sans", Arial, sans-serif;
}
.container-fields .field-value {
color: #00788E;
}
.container-fields details > summary {
cursor: pointer;
display: list-item;
}
.container-fields details > summary:hover {
color: #0A6EAA;
}
</style>
"""

JS_SCRIPT = """
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
console.log('Copied to clipboard: ' + text);
}, function(err) {
console.error('Could not copy text: ', err);
});
}
document.addEventListener('DOMContentLoaded', function() {
let fieldKeys = document.querySelectorAll('.container-fields .field-key');
fieldKeys.forEach(function(fieldKey) {
fieldKey.addEventListener('click', function() {
let accessCode = fieldKey.getAttribute('title').replace('Access code: ', '');
copyToClipboard(accessCode);
});
});
});
</script>
"""
if self.name == self.__class__.__name__:
header_text = self.name
else:
header_text = f"{self.name} ({self.__class__.__name__})"
html_repr = CSS_STYLE
html_repr += JS_SCRIPT
html_repr += "<div class='container-wrap'>"
html_repr += (
f"<div class='container-header'><div class='xr-obj-type'><h3>{header_text}</h3></div></div>"
)
html_repr += self._generate_html_repr(self.fields)
html_repr += "</div>"
return html_repr

def _generate_html_repr(self, fields, level=0, access_code=".fields"):
html_repr = ""

if isinstance(fields, dict):
for key, value in fields.items():
current_access_code = f"{access_code}['{key}']"
if (
isinstance(value, (list, dict, np.ndarray))
or hasattr(value, "fields")
):
html_repr += (
f'<details><summary style="display: list-item; margin-left: {level * 20}px;" '
f'class="container-fields field-key" title="{current_access_code}"><b>{key}</b></summary>'
)
if hasattr(value, "fields"):
value = value.fields
current_access_code = current_access_code + ".fields"
html_repr += self._generate_html_repr(
value, level + 1, current_access_code
)
html_repr += "</details>"
else:
html_repr += (
f'<div style="margin-left: {level * 20}px;" class="container-fields"><span class="field-key"'
f' title="{current_access_code}">{key}:</span> <span class="field-value">{value}</span></div>'
)
elif isinstance(fields, list):
for index, item in enumerate(fields):
current_access_code = f"{access_code}[{index}]"
html_repr += (
f'<div style="margin-left: {level * 20}px;" class="container-fields"><span class="field-value"'
f' title="{current_access_code}">{str(item)}</span></div>'
)
elif isinstance(fields, np.ndarray):
str_ = str(fields).replace("\n", "</br>")
html_repr += (
f'<div style="margin-left: {level * 20}px;" class="container-fields">{str_}</div>'
)
else:
pass

return html_repr

@staticmethod
def __smart_str(v, num_indent):
"""
Expand Down
75 changes: 60 additions & 15 deletions tests/unit/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ class Subcontainer(Container):
pass


class ContainerWithChild(Container):
__fields__ = ({'name': 'field1', 'child': True}, )

@docval({'name': 'field1', 'doc': 'field1 doc', 'type': None, 'default': None})
def __init__(self, **kwargs):
super().__init__('test name')
self.field1 = kwargs['field1']


class TestExternalResourcesManager(TestCase):
def test_link_and_get_resources(self):
em = ExternalResourcesManager()
Expand Down Expand Up @@ -263,6 +272,57 @@ def test_reset_parent_no_parent(self):
self.assertIsNone(obj.parent)


class TestHTMLRepr(TestCase):

class ContainerWithChildAndData(Container):
__fields__ = (
{'name': 'child', 'child': True},
"data",
"str"
)

@docval(
{'name': 'child', 'doc': 'field1 doc', 'type': Container},
{'name': "data", "doc": 'data', 'type': list, "default": None},
{'name': "str", "doc": 'str', 'type': str, "default": None},
)
def __init__(self, **kwargs):
super().__init__('test name')
self.child = kwargs['child']
self.data = kwargs['data']
self.str = kwargs['str']

def test_repr_html_(self):
child_obj1 = Container('test child 1')
obj1 = self.ContainerWithChildAndData(child=child_obj1, data=[1, 2, 3], str="hello")
assert obj1._repr_html_() == (
'\n <style>\n .container-fields {\n font-family: "Open Sans", Arial, sans-'
'serif;\n }\n .container-fields .field-value {\n color: #00788E;\n '
' }\n .container-fields details > summary {\n cursor: pointer;\n '
' display: list-item;\n }\n .container-fields details > summary:hover {\n '
' color: #0A6EAA;\n }\n </style>\n \n <script>\n functio'
'n copyToClipboard(text) {\n navigator.clipboard.writeText(text).then(function() {\n '
' console.log(\'Copied to clipboard: \' + text);\n }, function(err) {\n '
' console.error(\'Could not copy text: \', err);\n });\n }\n\n '
' document.addEventListener(\'DOMContentLoaded\', function() {\n let fieldKeys = document.q'
'uerySelectorAll(\'.container-fields .field-key\');\n fieldKeys.forEach(function(fieldKey) {'
'\n fieldKey.addEventListener(\'click\', function() {\n let acces'
'sCode = fieldKey.getAttribute(\'title\').replace(\'Access code: \', \'\');\n copyTo'
'Clipboard(accessCode);\n });\n });\n });\n </script>\n'
' <div class=\'container-wrap\'><div class=\'container-header\'><div class=\'xr-obj-type\'><h3>test '
'name (ContainerWithChildAndData)</h3></div></div><details><summary style="display: list-item; margin-left:'
' 0px;" class="container-fields field-key" title=".fields[\'child\']"><b>child</b></summary></details><deta'
'ils><summary style="display: list-item; margin-left: 0px;" class="container-fields field-key" title=".fiel'
'ds[\'data\']"><b>data</b></summary><div style="margin-left: 20px;" class="container-fields"><span class="f'
'ield-value" title=".fields[\'data\'][0]">1</span></div><div style="margin-left: 20px;" class="container-fi'
'elds"><span class="field-value" title=".fields[\'data\'][1]">2</span></div><div style="margin-left: 20px;"'
' class="container-fields"><span class="field-value" title=".fields[\'data\'][2]">3</span></div></details><'
'div style="margin-left: 0px;" class="container-fields"><span class="field-key" title=".fields[\'str\']">st'
'r:</span> <span class="field-value">hello</span></div></div>'
)


class TestData(TestCase):

def test_constructor_scalar(self):
Expand Down Expand Up @@ -507,14 +567,6 @@ def __init__(self, **kwargs):
self.assertIsNone(obj4.field1)

def test_child(self):
class ContainerWithChild(Container):
__fields__ = ({'name': 'field1', 'child': True}, )

@docval({'name': 'field1', 'doc': 'field1 doc', 'type': None, 'default': None})
def __init__(self, **kwargs):
super().__init__('test name')
self.field1 = kwargs['field1']

child_obj1 = Container('test child 1')
obj1 = ContainerWithChild(child_obj1)
self.assertIs(child_obj1.parent, obj1)
Expand All @@ -532,13 +584,6 @@ def __init__(self, **kwargs):
self.assertIsNone(obj2.field1)

def test_setter_set_modified(self):
class ContainerWithChild(Container):
__fields__ = ({'name': 'field1', 'child': True}, )

@docval({'name': 'field1', 'doc': 'field1 doc', 'type': None, 'default': None})
def __init__(self, **kwargs):
super().__init__('test name')
self.field1 = kwargs['field1']

child_obj1 = Container('test child 1')
obj1 = ContainerWithChild()
Expand Down

0 comments on commit 6043e77

Please sign in to comment.