Repository: silx-kit/silx
Issue: [convert] file series fail because of None in attrs

When merging a file series with *silx convert*, the process now fails because the "file" attribute *@filename* is *None*. 
This causes the following error:
`TypeError: Object dtype dtype('O') has no native HDF5 equivalent`

I think that previously it didn't crash because the was a coercion of all attributes to a dtype *numpy.string_*, so we ended up with a *@filename=b"None"*. 

I'm not sure if this is a `fabioh5` bug or a `silx.io.convert` bug.

In `silx.io.convert`, this patch fixes the issue:

```
diff --git a/silx/io/convert.py b/silx/io/convert.py
index 3af2c44..a2839fd 100644
--- a/silx/io/convert.py
+++ b/silx/io/convert.py
@@ -165,8 +165,9 @@ class Hdf5Writer(object):
         root_grp = h5f[self.h5path]
         for key in infile.attrs:
             if self.overwrite_data or key not in root_grp.attrs:
-                root_grp.attrs.create(key,
-                                      infile.attrs[key])
+                if infile.attrs[key] is not None:
+                    root_grp.attrs.create(key,
+                                          infile.attrs[key])
```
 
If we want to fix this In `fabioh5`, we could set the `@filename` attribute to "", or "None", or drop it altogether.


Changed files:
silx/io/fabioh5.py, silx/app/test/test_convert.py, silx/io/test/test_spectoh5.py, silx/io/test/test_spech5.py, silx/io/spech5.py, silx/io/convert.py

diff --git a/silx/app/test/test_convert.py b/silx/app/test/test_convert.py
index 8662a68a0..dca15050e 100644
--- a/silx/app/test/test_convert.py
+++ b/silx/app/test/test_convert.py
@@ -155,7 +155,7 @@ class TestConvertCommand(unittest.TestCase):
             if sys.version < '3.0':
                 title12 = title12.encode("utf-8")
             self.assertEqual(title12,
-                             "1 aaaaaa")
+                             "aaaaaa")
 
             creator = h5f.attrs.get("creator")
             self.assertIsNotNone(creator, "No creator attribute in NXroot group")
diff --git a/silx/io/convert.py b/silx/io/convert.py
index 3af2c4463..d60729b87 100644
--- a/silx/io/convert.py
+++ b/silx/io/convert.py
@@ -52,9 +52,11 @@ Text strings are written to the HDF5 datasets as variable-length utf-8.
 """
 
 import logging
+import numpy
 
 import silx.io
 from silx.io import is_dataset, is_group, is_softlink
+from silx.third_party import six
 
 __authors__ = ["P. Knobel"]
 __license__ = "MIT"
@@ -102,6 +104,22 @@ def _create_link(h5f, link_name, target_name,
         raise ValueError("link_type  must be 'hard' or 'soft'")
 
 
+def _attr_utf8(attr_value):
+    """If attr_value is bytes, make sure we output utf-8
+
+    :param attr_value: String (possibly bytes if PY2)
+    :return: Attr ready to be written by h5py as utf8
+    """
+    if isinstance(attr_value, six.binary_type):
+        out_attr_value = numpy.array(
+            attr_value,
+            dtype=h5py.special_dtype(vlen=six.text_type))
+    else:
+        out_attr_value = attr_value
+
+    return out_attr_value
+
+
 class Hdf5Writer(object):
     """Converter class to write the content of a data file to a HDF5 file.
     """
@@ -166,7 +184,7 @@ class Hdf5Writer(object):
         for key in infile.attrs:
             if self.overwrite_data or key not in root_grp.attrs:
                 root_grp.attrs.create(key,
-                                      infile.attrs[key])
+                                      _attr_utf8(infile.attrs[key]))
 
         # Handle links at the end, when their targets are created
         for link_name, target_name in self._links:
@@ -206,7 +224,8 @@ class Hdf5Writer(object):
             # add HDF5 attributes
             for key in obj.attrs:
                 if self.overwrite_data or key not in ds.attrs:
-                    ds.attrs.create(key, obj.attrs[key])
+                    ds.attrs.create(key,
+                                    _attr_utf8(obj.attrs[key]))
 
             if not self.overwrite_data and member_initially_exists:
                 _logger.warn("Not overwriting existing dataset: " + h5_name)
@@ -221,7 +240,8 @@ class Hdf5Writer(object):
             # add HDF5 attributes
             for key in obj.attrs:
                 if self.overwrite_data or key not in grp.attrs:
-                    grp.attrs.create(key, obj.attrs[key])
+                    grp.attrs.create(key,
+                                     _attr_utf8(obj.attrs[key]))
 
 
 def _is_commonh5_group(grp):
diff --git a/silx/io/fabioh5.py b/silx/io/fabioh5.py
index 5013108e9..14d88eaa5 100644
--- a/silx/io/fabioh5.py
+++ b/silx/io/fabioh5.py
@@ -1,6 +1,6 @@
 # coding: utf-8
 # /*##########################################################################
-# Copyright (C) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
 #
 # Permission is hereby granted, free of charge, to any person obtaining a copy
 # of this software and associated documentation files (the "Software"), to deal
@@ -869,8 +869,9 @@ class File(commonh5.File):
 
         attrs = {"NX_class": "NXroot",
                  "file_time": datetime.datetime.now().isoformat(),
-                 "file_name": file_name,
                  "creator": "silx %s" % silx_version}
+        if file_name is not None:
+            attrs["file_name"] = file_name
         commonh5.File.__init__(self, name=file_name, attrs=attrs)
         scan = self.create_scan_group(self.__fabio_reader)
         self.add_node(scan)
diff --git a/silx/io/spech5.py b/silx/io/spech5.py
index 390fef00d..6eb3e5685 100644
--- a/silx/io/spech5.py
+++ b/silx/io/spech5.py
@@ -589,10 +589,12 @@ class ScanGroup(commonh5.Group, SpecH5Group):
         commonh5.Group.__init__(self, scan_key, parent=parent,
                                 attrs={"NX_class": to_h5py_utf8("NXentry")})
 
-        self.add_node(SpecH5NodeDataset(
-                name="title",
-                data=to_h5py_utf8(scan.scan_header_dict["S"]),
-                parent=self))
+        # take title in #S after stripping away scan number and spaces
+        s_hdr_line = scan.scan_header_dict["S"]
+        title = s_hdr_line.lstrip("0123456789").lstrip()
+        self.add_node(SpecH5NodeDataset(name="title",
+                                        data=to_h5py_utf8(title),
+                                        parent=self))
 
         if "D" in scan.scan_header_dict:
             try:
@@ -653,13 +655,12 @@ class InstrumentSpecfileGroup(commonh5.Group, SpecH5Group):
                                 attrs={"NX_class": to_h5py_utf8("NXcollection")})
         self.add_node(SpecH5NodeDataset(
                 name="file_header",
-                data=to_h5py_utf8(
-                        "\\n".join(scan.file_header)),
+                data=to_h5py_utf8(scan.file_header),
                 parent=self,
                 attrs={}))
         self.add_node(SpecH5NodeDataset(
                 name="scan_header",
-                data=to_h5py_utf8("\\n".join(scan.scan_header)),
+                data=to_h5py_utf8(scan.scan_header),
                 parent=self,
                 attrs={}))
 
diff --git a/silx/io/test/test_spech5.py b/silx/io/test/test_spech5.py
index 6b9b1f530..861eb9e18 100644
--- a/silx/io/test/test_spech5.py
+++ b/silx/io/test/test_spech5.py
@@ -354,19 +354,19 @@ class TestSpecH5(unittest.TestCase):
         scan_header = self.sfh5["/1.2/instrument/specfile/scan_header"]
 
         # File header has 10 lines
-        self.assertEqual(len(file_header.split("\\n")), 10)
+        self.assertEqual(len(file_header), 10)
         # 1.2 has 9 scan & mca header lines
-        self.assertEqual(len(scan_header.split("\\n")), 9)
+        self.assertEqual(len(scan_header), 9)
 
         # line 4 of file header
         self.assertEqual(
-                file_header.split("\\n")[3],
+                file_header[3],
                 u"#C imaging  User = opid17")
         # line 4 of scan header
         scan_header = self.sfh5["25.1/instrument/specfile/scan_header"]
 
         self.assertEqual(
-                scan_header.split("\\n")[3],
+                scan_header[3],
                 u"#P1 4.74255 6.197579 2.238283")
 
     def testLinks(self):
@@ -479,7 +479,7 @@ class TestSpecH5(unittest.TestCase):
 
     def testTitle(self):
         self.assertEqual(self.sfh5["/25.1/title"],
-                         u"25  ascan  c3th 1.33245 1.52245  40 0.15")
+                         u"ascan  c3th 1.33245 1.52245  40 0.15")
 
     def testValues(self):
         group = self.sfh5["/25.1"]
diff --git a/silx/io/test/test_spectoh5.py b/silx/io/test/test_spectoh5.py
index 1e433cd15..046aa6be0 100644
--- a/silx/io/test/test_spectoh5.py
+++ b/silx/io/test/test_spectoh5.py
@@ -144,7 +144,7 @@ class TestConvertSpecHDF5(unittest.TestCase):
         """Test the value of a dataset"""
         title12 = self.h5f["/1.2/title"].value
         self.assertEqual(title12,
-                         u"1 aaaaaa")
+                         u"aaaaaa")
 
     def testAttrs(self):
         # Test root group (file) attributes