Skip to content

Commit faa724a

Browse files
0.6.2 (#33)
* v0.6.1 catch errors that cause crash on string load when strings are improperly formatted as arrays. Update for 2D sourceLabels * CI: Automated docs update * Accept 1D and 2D sourceLabels * CI: Automated docs update * Accidentally to define a thing * v0.6.2 made colorama and termcolor optional deps. Introduced file open modes and marked path-with-no-mode construction of SNIRF deprecated. Fixed sourceLabel validation * 0p6p2 fixes * CI: Automated docs update Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent f5b1c78 commit faa724a

File tree

7 files changed

+282
-186
lines changed

7 files changed

+282
-186
lines changed

docs/pysnirf2.md

Lines changed: 106 additions & 106 deletions
Large diffs are not rendered by default.

gen/footer.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ class Probe(Probe):
8686

8787
def _validate(self, result: ValidationResult):
8888

89+
# Override sourceLabels validation, can be 1D or 2D
90+
with h5py.File(TemporaryFile(), 'w') as tmp:
91+
if type(self._sourceLabels) in [type(_AbsentDataset), type(None)]:
92+
result._add(self.location + '/sourceLabels', 'OPTIONAL_DATASET_MISSING')
93+
else:
94+
try:
95+
if type(self._sourceLabels) is type(_PresentDataset) or 'sourceLabels' in self._h:
96+
dataset = self._h['sourceLabels']
97+
else:
98+
dataset = _create_dataset_string_array(tmp, 'sourceLabels', self._sourceLabels)
99+
result._add(self.location + '/sourceLabels', _validate_string_array(dataset, ndims=[1, 2]))
100+
except ValueError: # If the _create_dataset function can't convert the data
101+
result._add(self.location + '/sourceLabels', 'INVALID_DATASET_TYPE')
102+
89103
s2 = self.sourcePos2D is not None
90104
d2 = self.detectorPos2D is not None
91105
s3 = self.sourcePos3D is not None
@@ -139,6 +153,7 @@ def save(self, *args):
139153
if len(args) > 0 and type(args[0]) is str:
140154
path = args[0]
141155
if not path.endswith('.snirf'):
156+
path.replace('.', '')
142157
path += '.snirf'
143158
with h5py.File(path, 'w') as new_file:
144159
self._save(new_file)
@@ -205,7 +220,7 @@ def _validate(self, result: ValidationResult):
205220
for nirs in self.nirs:
206221
if type(nirs.probe) not in [type(None), type(_AbsentGroup)]:
207222
if nirs.probe.sourceLabels is not None:
208-
lenSourceLabels = nirs.probe.sourceLabels.size
223+
lenSourceLabels = nirs.probe.sourceLabels.shape[0]
209224
else:
210225
lenSourceLabels = 0
211226
if nirs.probe.detectorLabels is not None:
@@ -253,7 +268,7 @@ def loadSnirf(path: str, dynamic_loading: bool=False, logfile: bool=False) -> Sn
253268
if not path.endswith('.snirf'):
254269
path += '.snirf'
255270
if os.path.exists(path):
256-
return Snirf(path, dynamic_loading=dynamic_loading, logfile=logfile)
271+
return Snirf(path, 'r+', dynamic_loading=dynamic_loading, logfile=logfile)
257272
else:
258273
raise FileNotFoundError('No SNIRF file at ' + path)
259274

@@ -282,7 +297,7 @@ def validateSnirf(path: str) -> ValidationResult:
282297
if not path.endswith('.snirf'):
283298
path += '.snirf'
284299
if os.path.exists(path):
285-
with Snirf(path) as snirf:
300+
with Snirf(path, 'r') as snirf:
286301
return snirf.validate()
287302
else:
288303
raise FileNotFoundError('No SNIRF file at ' + path)

gen/header.py

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
from collections.abc import MutableSequence
2727
from tempfile import TemporaryFile
2828
import logging
29-
import termcolor
30-
import colorama
3129
from typing import Tuple
3230
import time
3331
import io
@@ -44,20 +42,32 @@
4442
raise ImportError('pysnirf2 requires Python > 3')
4543

4644

47-
class SnirfFormatError(Exception):
45+
class SnirfFormatError(Warning):
4846
"""Raised when SNIRF-specific error prevents file from loading properly."""
4947
pass
5048

5149

5250
# Colored prints for validation output to console
53-
if os.name == 'nt':
54-
colorama.init()
55-
56-
_printr = lambda x: termcolor.cprint(x, 'red')
57-
_printg = lambda x: termcolor.cprint(x, 'green')
58-
_printb = lambda x: termcolor.cprint(x, 'blue')
59-
_printm = lambda x: termcolor.cprint(x, 'magenta')
51+
try:
52+
import termcolor
53+
import colorama
6054

55+
if os.name == 'nt':
56+
colorama.init()
57+
58+
_printr = lambda x: termcolor.cprint(x, 'red')
59+
_printg = lambda x: termcolor.cprint(x, 'green')
60+
_printb = lambda x: termcolor.cprint(x, 'blue')
61+
_printm = lambda x: termcolor.cprint(x, 'magenta')
62+
_colored = termcolor.colored
63+
64+
except ImportError:
65+
_printr = lambda x: print(x)
66+
_printg = lambda x: print(x)
67+
_printb = lambda x: print(x)
68+
_printm = lambda x: print(x)
69+
_colored = lambda x, c: x
70+
6171

6272
def _isfilelike(o: object) -> bool:
6373
"""Returns True if object is an instance of a file-like object like `io.IOBase` or `io.BufferedIOBase`."""
@@ -290,10 +300,14 @@ def _read_string(dataset: h5py.Dataset) -> str:
290300
if type(dataset) is not h5py.Dataset:
291301
raise TypeError("'dataset' must be type h5py.Dataset")
292302
# Because many SNIRF files are saved with string values in length 1 arrays
293-
if dataset.ndim > 0:
294-
return str(dataset[0].decode('ascii'))
295-
else:
296-
return str(dataset[()].decode('ascii'))
303+
try:
304+
if dataset.ndim > 0:
305+
return str(dataset[0].decode('ascii'))
306+
else:
307+
return str(dataset[()].decode('ascii'))
308+
except AttributeError: # If we expected a string and got something else, `decode` isn't there
309+
warn('Expected dataset {} to be stringlike, is {} conversion may be incorrect'.format(dataset.name, dataset.dtype), SnirfFormatError)
310+
return str(dataset[0])
297311

298312

299313
def _read_int(dataset: h5py.Dataset) -> int:
@@ -562,9 +576,9 @@ def display(self, severity=2):
562576
s += issue.location.ljust(longest_key) + ' ' + _SEVERITY_LEVELS[sev] + ' ' + issue.name.ljust(longest_code) + '\n'
563577
print(s)
564578
for i in range(0, severity):
565-
[_printg, _printb, _printm, _printr][i]('Found ' + str(printed[i]) + ' ' + termcolor.colored(_SEVERITY_LEVELS[i], _SEVERITY_COLORS[i]) + ' (hidden)')
579+
[_printg, _printb, _printm, _printr][i]('Found ' + str(printed[i]) + ' ' + _colored(_SEVERITY_LEVELS[i], _SEVERITY_COLORS[i]) + ' (hidden)')
566580
for i in range(severity, 4):
567-
[_printg, _printb, _printm, _printr][i]('Found ' + str(printed[i]) + ' ' + termcolor.colored(_SEVERITY_LEVELS[i], _SEVERITY_COLORS[i]))
581+
[_printg, _printb, _printm, _printr][i]('Found ' + str(printed[i]) + ' ' + _colored(_SEVERITY_LEVELS[i], _SEVERITY_COLORS[i]))
568582
i = int(self.is_valid())
569583
[_printr, _printg][i]('\nFile is ' +['INVALID', 'VALID'][i])
570584

@@ -730,7 +744,7 @@ class SnirfConfig:
730744
def __init__(self):
731745
self.logger: logging.Logger = _logger # The logger that the interface will write to
732746
self.dynamic_loading: bool = False # If False, data is loaded in the constructor, if True, data is loaded on access
733-
747+
self.fmode: str = 'w' # 'w' or 'r', mode to open HDF5 file with
734748

735749
# Placeholder for a Dataset that is not on disk or in memory
736750
class _AbsentDatasetType():
@@ -812,8 +826,6 @@ def save(self, *args):
812826
else:
813827
if self._h != {}:
814828
file = self._h.file
815-
if file.mode not in ['r+', 'w']:
816-
raise ValueError('{} not writeable.', file) # TODO raise UnsupportedOperation
817829
self._save(file)
818830
self._cfg.logger.info('IndexedGroup-level save of %s at %s in %s', self.__class__.__name__,
819831
self._parent.location, self.filename)
@@ -1019,6 +1031,7 @@ def save(self, *args):
10191031
elif type(args[0]) is str:
10201032
path = args[0]
10211033
if not path.endswith('.snirf'):
1034+
path.replace('.', '')
10221035
path += '.snirf'
10231036
if os.path.exists(path):
10241037
file = h5py.File(path, 'w')
@@ -1031,8 +1044,6 @@ def save(self, *args):
10311044
else:
10321045
if self._parent._h != {}:
10331046
file = self._parent._h.file
1034-
if file.mode not in ['r+', 'w']:
1035-
raise ValueError('{} not writeable.', file) # TODO raise UnsupportedOperation
10361047
self._save(file)
10371048
self._cfg.logger.info('IndexedGroup-level save of %s at %s in %s', self.__class__.__name__,
10381049
self._parent.location, self.filename)

gen/pysnirf2.jinja

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,23 +359,44 @@ class Snirf(Group):
359359
def __init__(self, *args, dynamic_loading: bool = False, logfile: bool = False):
360360
self._cfg = SnirfConfig()
361361
self._cfg.dynamic_loading = dynamic_loading
362+
self._cfg.fmode = ''
362363
if logfile:
363364
self._cfg.logger = _create_logger(path, path.split('.')[0] + '.log')
364365
else:
365366
self._cfg.logger = _logger # Use global logger
366367
if len(args) > 0:
367368
path = args[0]
369+
if len(args) > 1:
370+
assert type(args[1]) is str, 'Positional argument 2 must be "r"/"w" mode'
371+
if args[1] == 'r':
372+
self._cfg.fmode = 'r'
373+
elif args[1] == 'r+':
374+
self._cfg.fmode = 'r+'
375+
elif args[1] == 'w':
376+
self._cfg.fmode = 'w'
377+
else:
378+
raise ValueError("Invalid mode: '{}'. Only 'r', 'r+' and 'w' are supported.".format(args[1]))
379+
else:
380+
warn('Use `Snirf(<path>, <mode>)` to open SNIRF file from path. Path-only construction is deprecated.', DeprecationWarning)
381+
# fmode is ''
368382
if type(path) is str:
369383
if not path.endswith('.snirf'):
384+
path.replace('.', '')
370385
path = path + '.snirf'
371386
if os.path.exists(path):
372387
self._cfg.logger.info('Loading from file %s', path)
373-
self._h = h5py.File(path, 'r+')
388+
if self._cfg.fmode == '':
389+
self._cfg.fmode = 'r+'
390+
self._h = h5py.File(path, self._cfg.fmode)
374391
else:
375392
self._cfg.logger.info('Creating new file at %s', path)
376-
self._h = h5py.File(path, 'w')
393+
if self._cfg.fmode == '':
394+
self._cfg.fmode = 'w'
395+
self._h = h5py.File(path, self._cfg.fmode)
377396
elif _isfilelike(args[0]):
378397
self._cfg.logger.info('Loading from filelike object')
398+
if self._cfg.fmode == '':
399+
self._cfg.fmode = 'r'
379400
self._h = h5py.File(path, 'r')
380401

381402
else:
@@ -384,6 +405,7 @@ class Snirf(Group):
384405
self._cfg.logger = _logger
385406
self._cfg.logger.info('Created Snirf object based on tempfile')
386407
path = None
408+
self._cfg.fmode = 'w'
387409
self._h = h5py.File(TemporaryFile(), 'w')
388410
{{ declare_members(ROOT) | indent }}
389411
{{ init_members(ROOT) | indent }}

pysnirf2/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__VERSION__ = (0, 6, 0)
1+
__VERSION__ = (0, 6, 2)
22
__version__ = '.'.join(map(str, __VERSION__))

0 commit comments

Comments
 (0)