Skip to content

Commit b273603

Browse files
committed
Merge branch 'events2bids'
2 parents cff515e + a8be713 commit b273603

File tree

15 files changed

+1291
-486
lines changed

15 files changed

+1291
-486
lines changed

bidscoin/__init__.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
if not (pluginfolder/plugin.name).is_file() and not plugin.name.startswith('_'):
8585
print(f"-> {pluginfolder/plugin.name}")
8686
shutil.copyfile(plugin, pluginfolder/plugin.name)
87-
for template in list((bidscoinroot/'heuristics').glob('*.yaml')) + [bidscoinroot/'heuristics'/'schema.json']:
87+
for template in [*(bidscoinroot/'heuristics').glob('*.yaml')] + [bidscoinroot/'heuristics'/'schema.json']:
8888
if not (templatefolder/template.name).is_file():
8989
print(f"-> {templatefolder/template.name}")
9090
shutil.copyfile(template, templatefolder/template.name)
@@ -131,6 +131,16 @@ def bidsversion() -> str:
131131
return (schemafolder/'BIDS_VERSION').read_text().strip()
132132

133133

134+
def is_hidden(path: Path):
135+
"""Checks if the filename or one of its parent folders is hidden"""
136+
137+
hidden = any(part.startswith('.') for part in path.parts)
138+
if hidden:
139+
LOGGER.verbose(f"Ignoring hidden file/folder: {path}")
140+
141+
return hidden
142+
143+
134144
def lsdirs(folder: Path, wildcard: str='*') -> List[Path]:
135145
"""
136146
Gets all sorted directories in a folder, ignores files. Foldernames starting with a dot are considered hidden and will be skipped
@@ -140,9 +150,6 @@ def lsdirs(folder: Path, wildcard: str='*') -> List[Path]:
140150
:return: A list with all directories in the folder
141151
"""
142152

143-
# Checks if a path is or contains a hidden rootfolder
144-
is_hidden = lambda path: any([part.startswith('.') for part in path.parts])
145-
146153
return sorted([item for item in sorted(folder.glob(wildcard)) if item.is_dir() and not is_hidden(item.relative_to(folder))])
147154

148155

bidscoin/bcoin.py

Lines changed: 96 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
yaml = YAML()
3232
yaml.representer.ignore_aliases = lambda *data: True # Expand aliases (https://stackoverflow.com/questions/58091449/disabling-alias-for-yaml-file-in-python)
3333

34-
LOGGER = logging.getLogger(__name__)
34+
# Define custom logging levels
35+
BCDEBUG, BCDEBUG_LEVEL = 'BCDEBUG', 11 # NB: using the standard debug mode will generate may debug messages from imports
36+
VERBOSE, VERBOSE_LEVEL = 'VERBOSE', 15
37+
SUCCESS, SUCCESS_LEVEL = 'SUCCESS', 25
3538

3639

3740
class TqdmUpTo(tqdm):
@@ -50,76 +53,39 @@ def update_to(self, b=1, bsize=1, tsize=None):
5053
self.update(b * bsize - self.n) # will also set self.n = b * bsize
5154

5255

53-
def drmaa_nativespec(specs: str, session) -> str:
54-
"""
55-
Converts (CLI default) native Torque walltime and memory specifications to the DRMAA implementation (currently only Slurm is supported)
56+
class CustomLogger(logging.Logger):
57+
"""Extend the Logger class to add custom methods for the new levels"""
5658

57-
:param specs: Native Torque walltime and memory specifications, e.g. '-l walltime=00:10:00,mem=2gb'
58-
:param session: The DRMAA session
59-
:return: The converted native specifications
60-
"""
61-
62-
jobmanager: str = session.drmaaImplementation
63-
64-
if '-l ' in specs and 'pbs' not in jobmanager.lower():
65-
66-
if 'slurm' in jobmanager.lower():
67-
specs = (specs.replace('-l ', '')
68-
.replace(',', ' ')
69-
.replace('walltime', '--time')
70-
.replace('mem', '--mem')
71-
.replace('gb','000'))
72-
else:
73-
LOGGER.warning(f"Default `--cluster` native specifications are not (yet) provided for {jobmanager}. Please add them to your command if you get DRMAA errors")
74-
specs = ''
59+
def bcdebug(self, message, *args, **kwargs):
60+
"""Custom BIDSCOIN DEBUG messages"""
61+
if self.isEnabledFor(BCDEBUG_LEVEL):
62+
self._log(BCDEBUG_LEVEL, message, args, **kwargs)
7563

76-
return specs.strip()
64+
def verbose(self, message, *args, **kwargs):
65+
"""Custom BIDSCOIN VERBOSE messages"""
66+
if self.isEnabledFor(VERBOSE_LEVEL):
67+
self._log(VERBOSE_LEVEL, message, args, **kwargs)
7768

69+
def success(self, message, *args, **kwargs):
70+
"""Custom BIDSCOIN SUCCESS messages"""
71+
if self.isEnabledFor(SUCCESS_LEVEL):
72+
self._log(SUCCESS_LEVEL, message, args, **kwargs)
7873

79-
def synchronize(pbatch, jobids: list, wait: int=15):
80-
"""
81-
Shows tqdm progress bars for queued and running DRMAA jobs. Waits until all jobs have finished +
82-
some extra wait time to give NAS systems the opportunity to fully synchronize
8374

84-
:param pbatch: The DRMAA session
85-
:param jobids: The job ids
86-
:param wait: The extra wait time for the NAS
87-
:return:
88-
"""
89-
90-
with logging_redirect_tqdm():
91-
92-
qbar = tqdm(total=len(jobids), desc='Queued ', unit='job', leave=False, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}]')
93-
rbar = tqdm(total=len(jobids), desc='Running', unit='job', leave=False, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}]', colour='green')
94-
done = 0
95-
while done < len(jobids):
96-
jobs = [pbatch.jobStatus(jobid) for jobid in jobids]
97-
done = sum(status in ('done', 'failed', 'undetermined') for status in jobs)
98-
qbar.n = sum(status == 'queued_active' for status in jobs)
99-
rbar.n = sum(status == 'running' for status in jobs)
100-
qbar.refresh(), rbar.refresh()
101-
time.sleep(2)
102-
qbar.close(), rbar.close()
103-
104-
failedjobs = [jobid for jobid in jobids if pbatch.jobStatus(jobid)=='failed']
105-
if failedjobs:
106-
LOGGER.error(f"{len(failedjobs)} HPC jobs failed to run:\n{failedjobs}\nThis may well be due to an underspecified `--cluster` input option (e.g. not enough memory)")
107-
108-
# Give NAS systems some time to fully synchronize
109-
for t in tqdm(range(wait*100), desc='synchronizing', leave=False, bar_format='{l_bar}{bar}| [{elapsed}]'):
110-
time.sleep(.01)
75+
# Get a logger from the custom logger class
76+
logging.setLoggerClass(CustomLogger)
77+
LOGGER = logging.getLogger(__name__)
11178

11279

11380
def setup_logging(logfile: Path=Path()):
11481
"""
11582
Set up the logging framework:
116-
1) Add a 'bcdebug', 'verbose' and a 'success' logging level
117-
2) Add a console streamhandler
118-
3) If logfile then add a normal log and a warning/error filehandler
83+
1) Add custom logging levels: 'bcdebug', 'verbose', and 'success'.
84+
2) Add a console stream handler for generating terminal output.
85+
3) Optionally add file handlers for normal log and warning/error log if logfile is provided.
11986
120-
:param logfile: Name of the logfile
121-
:return:
122-
"""
87+
:param logfile: Path to the logfile. If none, logging is console-only
88+
"""
12389

12490
# Set the default formats
12591
if DEBUG:
@@ -130,36 +96,17 @@ def setup_logging(logfile: Path=Path()):
13096
cfmt = '%(levelname)s | %(message)s'
13197
datefmt = '%Y-%m-%d %H:%M:%S'
13298

133-
# Add a BIDScoin debug logging level = 11 (NB: using the standard debug mode will generate may debug messages from imports)
134-
logging.BCDEBUG = 11
135-
logging.addLevelName(logging.BCDEBUG, 'BCDEBUG')
136-
logging.__all__ += ['BCDEBUG'] if 'BCDEBUG' not in logging.__all__ else []
137-
def bcdebug(self, message, *args, **kws):
138-
if self.isEnabledFor(logging.BCDEBUG): self._log(logging.BCDEBUG, message, args, **kws)
139-
logging.Logger.bcdebug = bcdebug
140-
141-
# Add a verbose logging level = 15
142-
logging.VERBOSE = 15
143-
logging.addLevelName(logging.VERBOSE, 'VERBOSE')
144-
logging.__all__ += ['VERBOSE'] if 'VERBOSE' not in logging.__all__ else []
145-
def verbose(self, message, *args, **kws):
146-
if self.isEnabledFor(logging.VERBOSE): self._log(logging.VERBOSE, message, args, **kws)
147-
logging.Logger.verbose = verbose
148-
149-
# Add a success logging level = 25
150-
logging.SUCCESS = 25
151-
logging.addLevelName(logging.SUCCESS, 'SUCCESS')
152-
logging.__all__ += ['SUCCESS'] if 'SUCCESS' not in logging.__all__ else []
153-
def success(self, message, *args, **kws):
154-
if self.isEnabledFor(logging.SUCCESS): self._log(logging.SUCCESS, message, args, **kws)
155-
logging.Logger.success = success
156-
157-
# Set the root logging level
99+
# Add custom log levels to logging
100+
logging.addLevelName(BCDEBUG_LEVEL, BCDEBUG)
101+
logging.addLevelName(VERBOSE_LEVEL, VERBOSE)
102+
logging.addLevelName(SUCCESS_LEVEL, SUCCESS)
103+
104+
# Get the root logger and set the appropriate level
158105
logger = logging.getLogger()
159-
logger.setLevel('BCDEBUG' if DEBUG else 'VERBOSE')
106+
logger.setLevel(BCDEBUG_LEVEL if DEBUG else VERBOSE_LEVEL)
160107

161108
# Add the console streamhandler and bring some color to those boring logs! :-)
162-
coloredlogs.install(level='BCDEBUG' if DEBUG else 'VERBOSE' if not logfile.name else 'INFO', fmt=cfmt, datefmt=datefmt) # NB: Using tqdm sets the streamhandler level to 0, see: https://github.com/tqdm/tqdm/pull/1235
109+
coloredlogs.install(level=BCDEBUG if DEBUG else VERBOSE if not logfile.name else 'INFO', fmt=cfmt, datefmt=datefmt) # NB: Using tqdm sets the streamhandler level to 0, see: https://github.com/tqdm/tqdm/pull/1235
163110
coloredlogs.DEFAULT_LEVEL_STYLES['verbose']['color'] = 245 # = Gray
164111

165112
if logfile.name:
@@ -168,7 +115,7 @@ def success(self, message, *args, **kws):
168115
logfile.parent.mkdir(parents=True, exist_ok=True) # Create the log dir if it does not exist
169116
formatter = logging.Formatter(fmt=fmt, datefmt=datefmt)
170117
loghandler = logging.FileHandler(logfile)
171-
loghandler.setLevel('BCDEBUG')
118+
loghandler.setLevel(BCDEBUG)
172119
loghandler.setFormatter(formatter)
173120
loghandler.set_name('loghandler')
174121
logger.addHandler(loghandler)
@@ -219,6 +166,66 @@ def reporterrors() -> str:
219166
return errors
220167

221168

169+
def drmaa_nativespec(specs: str, session) -> str:
170+
"""
171+
Converts (CLI default) native Torque walltime and memory specifications to the DRMAA implementation (currently only Slurm is supported)
172+
173+
:param specs: Native Torque walltime and memory specifications, e.g. '-l walltime=00:10:00,mem=2gb'
174+
:param session: The DRMAA session
175+
:return: The converted native specifications
176+
"""
177+
178+
jobmanager: str = session.drmaaImplementation
179+
180+
if '-l ' in specs and 'pbs' not in jobmanager.lower():
181+
182+
if 'slurm' in jobmanager.lower():
183+
specs = (specs.replace('-l ', '')
184+
.replace(',', ' ')
185+
.replace('walltime', '--time')
186+
.replace('mem', '--mem')
187+
.replace('gb','000'))
188+
else:
189+
LOGGER.warning(f"Default `--cluster` native specifications are not (yet) provided for {jobmanager}. Please add them to your command if you get DRMAA errors")
190+
specs = ''
191+
192+
return specs.strip()
193+
194+
195+
def synchronize(pbatch, jobids: list, wait: int=15):
196+
"""
197+
Shows tqdm progress bars for queued and running DRMAA jobs. Waits until all jobs have finished +
198+
some extra wait time to give NAS systems the opportunity to fully synchronize
199+
200+
:param pbatch: The DRMAA session
201+
:param jobids: The job ids
202+
:param wait: The extra wait time for the NAS
203+
:return:
204+
"""
205+
206+
with logging_redirect_tqdm():
207+
208+
qbar = tqdm(total=len(jobids), desc='Queued ', unit='job', leave=False, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}]')
209+
rbar = tqdm(total=len(jobids), desc='Running', unit='job', leave=False, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}]', colour='green')
210+
done = 0
211+
while done < len(jobids):
212+
jobs = [pbatch.jobStatus(jobid) for jobid in jobids]
213+
done = sum(status in ('done', 'failed', 'undetermined') for status in jobs)
214+
qbar.n = sum(status == 'queued_active' for status in jobs)
215+
rbar.n = sum(status == 'running' for status in jobs)
216+
qbar.refresh(), rbar.refresh()
217+
time.sleep(2)
218+
qbar.close(), rbar.close()
219+
220+
failedjobs = [jobid for jobid in jobids if pbatch.jobStatus(jobid)=='failed']
221+
if failedjobs:
222+
LOGGER.error(f"{len(failedjobs)} HPC jobs failed to run:\n{failedjobs}\nThis may well be due to an underspecified `--cluster` input option (e.g. not enough memory)")
223+
224+
# Give NAS systems some time to fully synchronize
225+
for t in tqdm(range(wait*100), desc='synchronizing', leave=False, bar_format='{l_bar}{bar}| [{elapsed}]'):
226+
time.sleep(.01)
227+
228+
222229
def list_executables(show: bool=False) -> list:
223230
"""
224231
:param show: Print the installed console scripts if True
@@ -410,7 +417,7 @@ def import_plugin(plugin: Union[Path,str], functions: tuple=()) -> Union[types.M
410417
functionsfound.append(function)
411418

412419
if functions and not functionsfound:
413-
LOGGER.info(f"Plugin '{plugin}' does not contain {functions} functions")
420+
LOGGER.bcdebug(f"Plugin '{plugin}' does not contain {functions} functions")
414421
else:
415422
return module
416423

0 commit comments

Comments
 (0)