diff --git a/CHANGELOG.md b/CHANGELOG.md index 9016dbb2e..6a3a79232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # PyNWB Changelog +## PyNWB 2.6.0 (Upcoming) + +### Enhancements and minor changes +- Add `NWBHDF5IO.can_read()`. @bendichter [#1703](https://github.com/NeurodataWithoutBorders/pynwb/pull/1703) +- Add `pynwb.get_nwbfile_version()`. @bendichter [#1703](https://github.com/NeurodataWithoutBorders/pynwb/pull/1703) + ## PyNWB 2.5.0 (August 18, 2023) ### Enhancements and minor changes diff --git a/src/pynwb/__init__.py b/src/pynwb/__init__.py index a6195e779..710f55ee8 100644 --- a/src/pynwb/__init__.py +++ b/src/pynwb/__init__.py @@ -148,6 +148,33 @@ def _dec(cls): _dec(container_cls) +def get_nwbfile_version(h5py_file: h5py.File): + """ + Get the NWB version of the file if it is an NWB file. + :returns: Tuple consisting of: 1) the original version string as stored in the file and + 2) a tuple with the parsed components of the version string, consisting of integers + and strings, e.g., (2, 5, 1, beta). (None, None) will be returned if the file is not a valid NWB file + or the nwb_version is missing, e.g., in the case when no data has been written to the file yet. + """ + # Get the version string for the NWB file + try: + nwb_version_string = h5py_file.attrs['nwb_version'] + # KeyError occurs when the file is empty (e.g., when creating a new file nothing has been written) + # or when the HDF5 file is not a valid NWB file + except KeyError: + return None, None + # Other system may have written nwb_version as a fixed-length string, resulting in a numpy.bytes_ object + # on read, rather than a variable-length string. To address this, decode the bytes if necessary. + if not isinstance(nwb_version_string, str): + nwb_version_string = nwb_version_string.decode() + + # Parse the version string + nwb_version_parts = nwb_version_string.replace("-", ".").replace("_", ".").split(".") + nwb_version = tuple([int(i) if i.isnumeric() else i + for i in nwb_version_parts]) + return nwb_version_string, nwb_version + + # a function to register an object mapper for a container class @docval({"name": "container_cls", "type": type, "doc": "the Container class for which the given ObjectMapper class gets used"}, @@ -201,6 +228,17 @@ def get_sum(self, a, b): class NWBHDF5IO(_HDF5IO): + @staticmethod + def can_read(path: str): + """Determine whether a given path is readable by this class""" + if not os.path.isfile(path): # path is file that exists + return False + try: + with h5py.File(path, "r") as file: # path is HDF5 file + return get_nwbfile_version(file)[1][0] >= 2 # Major version of NWB >= 2 + except IOError: + return False + @docval({'name': 'path', 'type': (str, Path), 'doc': 'the path to the HDF5 file', 'default': None}, {'name': 'mode', 'type': str, 'doc': 'the mode to open the HDF5 file with, one of ("w", "r", "r+", "a", "w-", "x")', @@ -263,23 +301,7 @@ def nwb_version(self): and strings, e.g., (2, 5, 1, beta). (None, None) will be returned if the nwb_version is missing, e.g., in the case when no data has been written to the file yet. """ - # Get the version string for the NWB file - try: - nwb_version_string = self._file.attrs['nwb_version'] - # KeyError occurs when the file is empty (e.g., when creating a new file nothing has been written) - # or when the HDF5 file is not a valid NWB file - except KeyError: - return None, None - # Other system may have written nwb_version as a fixed-length string, resulting in a numpy.bytes_ object - # on read, rather than a variable-length string. To address this, decode the bytes if necessary. - if not isinstance(nwb_version_string, str): - nwb_version_string = nwb_version_string.decode() - - # Parse the version string - nwb_version_parts = nwb_version_string.replace("-", ".").replace("_", ".").split(".") - nwb_version = tuple([int(i) if i.isnumeric() else i - for i in nwb_version_parts]) - return nwb_version_string, nwb_version + return get_nwbfile_version(self._file) @docval(*get_docval(_HDF5IO.read), {'name': 'skip_version_check', 'type': bool, 'doc': 'skip checking of NWB version', 'default': False})