Commit 5caacdbd authored by Eric Pellegrini's avatar Eric Pellegrini
Browse files

added json file support for HDF dumped data

refactored HDFViewer class
updated demo notebook accordingly
parent 39e94c4f
......@@ -5,26 +5,26 @@
```
%% Cell type:code id: tags:
``` python
import h5py
from hdfviewer.widgets.HDFViewer import HDFViewerWidget
from hdfviewer.widgets.PathSelector import PathSelector
```
%% Cell type:code id: tags:
``` python
path = PathSelector(extensions=[".hdf",".h5",".nxs"])
path.widget
selectedPath = PathSelector(extensions=[".hdf",".h5",".nxs"])
selectedPath.widget
```
%% Cell type:code id: tags:
``` python
if path.file:
hdf = h5py.File(path.file,"r")
widget = HDFViewerWidget(hdf)
if selectedPath.path:
widget = HDFViewerWidget(selectedPath.path)
display(widget)
```
%%%% Output: display_data
......
import binascii
import io
import json
import os
import webbrowser
......@@ -10,26 +12,90 @@ import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.core.display import display
from hdfviewer import __version__
from hdfviewer.viewers.MplDataViewer import MplDataViewer, MplDataViewerError
from hdfviewer.widgets.MplOutput import MplOutput
def HDFViewerWidget(hdf, startPath=None):
class HDFViewerError(Exception):
"""Error handler for :mod:`HDFViewer` related exceptions.
"""
pass
def _openHDFFile(filename):
"""Open a HDF file.
The file can be a *true* HDF file or a json file in which a HDF has been dumped into.
:param hdfSource: the path to the file storing the HDF contents
:type hdfSource: str
The file can be a path to a *true* HDF file or to a file in which the HDF contents has been dumped into.
:raises: :class:`TypeError`: if the input is not a str
:raises: :class:`IOError`: if the path is not valid or could not be opened through h5py API
"""
hdf = None
if not isinstance(filename, str):
raise TypeError("invalid input type")
if not os.path.isfile(filename):
raise IOError("the input is not a file")
# First try to open the file as a HDF5 file
try:
hdf = h5py.File(filename, "r")
# Any exception should be caught at this level
except:
# Try to open it as a json dumped HDF file
# The JSON must contain the str version
with open(filename, "r") as f:
d = json.load(f)
byt = binascii.a2b_base64(d["data"])
try:
hdf = h5py.File(io.BytesIO(byt), "r")
# Any exception should be caught at this level
except:
pass
finally:
return hdf
def HDFViewerWidget(filename, startingPath=None):
"""Helper function that displays a :class:`HDFViewer` widget from a file.
The file can be a *true* HDF file or a json file in which a HDF has been dumped into.
:param hdf: the path to the file storing the HDF contents
:type hdf: str
:param startPath: the hdf path from where the HDF data will be inspected.
If not set, the starting path will be the root of the HDF data
:type startPath: str or None
"""
vbox = widgets.VBox()
button = widgets.Button(description="documentation", tooltip="open documentation for release {0}".format(__version__))
button = widgets.Button(description="documentation",
tooltip="open documentation for release {0}".format(__version__))
button.on_click(lambda event : HDFViewer.info())
button.on_click(lambda event: HDFViewer.info())
vbox.children = [button,HDFViewer(hdf,startPath)]
hdf = _openHDFFile(filename)
if hdf is None:
raise HDFViewerError(
"An error occured when reading {!r} file".format(filename))
return vbox
vbox.children = [button, HDFViewer(hdf, startingPath)]
class HDFViewerError(Exception):
""":class:`HDFViewer` specific exception"""
return vbox
pass
class HDFViewer(widgets.Accordion):
"""This class allows to inspect HDF data in the context of **Jupyter Lab**
......@@ -42,108 +108,101 @@ class HDFViewer(widgets.Accordion):
:start-after: usage-begin
:end-before: usage-end
:param hdf: the hdf Data to be inspected.
- for `str` input type, this will be the path to the HDF datafile
- for `File`, this will be the output of a prior HDF file opening in read mode
- for `bytes`, this will the byte representation of athe HDF data
:type hdf: str or bytes or :class:`h5py.File`
:param hdf: the hdf data file to be inspected.
:type hdf: :class:`h5py.File`
:param startPath: the hdf path from where the HDF data will be inspected.
If not set, the starting path will be the root of the HDF data
:type startPath: str or None
:raises: :class:`HDFViewerError`: if the HDF data could not be set properly
"""
def __init__(self, hdf, startPath=None):
widgets.Accordion.__init__(self)
try:
if isinstance(hdf,h5py.File) and hdf.mode=="r":
self._hdf = hdf
elif isinstance(hdf,str):
self._hdf = h5py.File(hdf,"r")
elif isinstance(hdf,bytes):
self._hdf = h5py.File(io.BytesIO(hdf),"r")
else:
raise IOError("Invalid HDF stream")
except Exception as e:
raise HDFViewerError(str(e))
self._hdf = hdf
if startPath is None:
self._startPath = "/"
self.children = [HDFViewer(hdf,self._startPath)]
self.set_title(0,self._startPath)
self.children = [HDFViewer(self._hdf, self._startPath)]
self.set_title(0, self._startPath)
else:
self._startPath = startPath
attributesAccordion = widgets.Accordion()
for idx,(key,value) in enumerate(self._hdf[self._startPath].attrs.items()):
attributesAccordion.children = list(attributesAccordion.children) + [widgets.HTML(value)]
attributesAccordion.set_title(idx,key)
for idx, (key, value) in enumerate(self._hdf[self._startPath].attrs.items()):
attributesAccordion.children = list(
attributesAccordion.children) + [widgets.HTML(value)]
attributesAccordion.set_title(idx, key)
# Setup the groups and datasets accordion
groupsAccordion = widgets.Accordion()
datasetsAccordion = widgets.Accordion()
for value in list(self._hdf[self._startPath].values()):
if isinstance(value,h5py.Group):
groupsAccordion.children = list(groupsAccordion.children) + [HDFViewer(hdf,value.name)]
groupsAccordion.set_title(len(groupsAccordion.children)-1,value.name)
elif isinstance(value,h5py.Dataset):
for value in list(self._hdf[self._startPath].values()):
if isinstance(value, h5py.Group):
groupsAccordion.children = list(
groupsAccordion.children) + [HDFViewer(hdf, value.name)]
groupsAccordion.set_title(
len(groupsAccordion.children)-1, value.name)
elif isinstance(value, h5py.Dataset):
datasetInfo = []
shape = value.shape
# Set some informations about the current hdf value
datasetInfo.append("<i>Dimension: %s</i>" % str(shape))
datasetInfo.append("<i>Reduced dimension: %s</i>" % str(tuple([s for s in shape if s != 1])))
datasetInfo.append("<i>Reduced dimension: %s</i>" %
str(tuple([s for s in shape if s != 1])))
datasetInfo.append("<i>Type: %s</i>" % value.dtype.name)
datasetInfo = "<br>".join(datasetInfo)
vbox = widgets.VBox()
vbox.children = [widgets.HTML(datasetInfo),MplOutput()]
datasetsAccordion.children = list(datasetsAccordion.children) + [vbox]
datasetsAccordion.set_title(len(datasetsAccordion.children)-1,value.name)
datasetsAccordion.observe(self._onSelectDataset,names="selected_index")
vbox.children = [widgets.HTML(datasetInfo), MplOutput()]
datasetsAccordion.children = list(
datasetsAccordion.children) + [vbox]
datasetsAccordion.set_title(
len(datasetsAccordion.children)-1, value.name)
datasetsAccordion.observe(
self._onSelectDataset, names="selected_index")
# Display only the accordions which have children
nestedAccordions = [("attributes",attributesAccordion),("groups",groupsAccordion),("datasets",datasetsAccordion)]
for title,acc in nestedAccordions:
nestedAccordions = [("attributes", attributesAccordion),
("groups", groupsAccordion), ("datasets", datasetsAccordion)]
for title, acc in nestedAccordions:
if not acc.children:
continue
acc.selected_index = None
self.children = list(self.children) + [acc]
self.set_title(len(self.children)-1,title)
self.set_title(len(self.children)-1, title)
# By default, the accordion is closed at start-up
self.selected_index = None
def _onSelectDataset(self,change):
def _onSelectDataset(self, change):
"""A callable that is called when a new dataset is selected
See `here <https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Events.html#Traitlet-events>`__ for more information
#Traitlet-events>`__ for more information
See `here <https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Events.html
"""
idx = change["new"]
# If the accordions is closed does nothing
if idx is None:
return
accordion = change["owner"]
path = accordion.get_title(idx)
vbox = accordion.children[idx]
output = vbox.children[1]
output.clear_output(wait=False)
with output:
try:
self._viewer = MplDataViewer(self._hdf[path],standAlone=False)
self._viewer = MplDataViewer(self._hdf[path], standAlone=False)
except MplDataViewerError as e:
label = widgets.Label(value=str(e))
display(label)
display(label)
else:
# Bind the DataViewer figure to the MplOutput widget for allowing a "clean" output clearing (i.e. release the figure from plt)
output.figure = self._viewer.viewer.figure
......@@ -153,7 +212,7 @@ class HDFViewer(widgets.Accordion):
"""Open the url of a given release of the package documentation.
:param release: the release of the package.
If None, the current release will be selected.
:type release: str
"""
......@@ -161,6 +220,6 @@ class HDFViewer(widgets.Accordion):
version = version if version else __version__
# This will open the package documentation stored on readthedocs
url = os.path.join("https://hdf-viewer.readthedocs.io/en/{0}".format(version))
url = os.path.join(
"https://hdf-viewer.readthedocs.io/en/{0}".format(version))
webbrowser.open(url)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment