From a45d0d1d845ad79c5459214442e11e6bc894fcff Mon Sep 17 00:00:00 2001 From: Marcel Zwiers Date: Tue, 10 Dec 2024 18:16:48 +0100 Subject: [PATCH] Bugfix for refactoring of IO helper functions (`bidscoin.plugins` -> `bidscoin.utilities`) + minor tweaks --- bidscoin/bcoin.py | 16 +++++++++++++++- bidscoin/bids.py | 15 ++++++++++++--- bidscoin/bidscoiner.py | 2 +- bidscoin/bidseditor.py | 7 ++++--- bidscoin/bidsmapper.py | 2 +- bidscoin/cli/_bcoin.py | 1 + bidscoin/cli/_dicomsort.py | 2 +- bidscoin/cli/_rawmapper.py | 2 +- bidscoin/plugins/__init__.py | 5 ++++- bidscoin/plugins/dcm2niix2bids.py | 2 ++ bidscoin/utilities/dicomsort.py | 2 -- docs/installation.rst | 1 + docs/utilities.rst | 6 +++--- 13 files changed, 46 insertions(+), 17 deletions(-) diff --git a/bidscoin/bcoin.py b/bidscoin/bcoin.py index 9631b16a..c16d951e 100755 --- a/bidscoin/bcoin.py +++ b/bidscoin/bcoin.py @@ -26,7 +26,7 @@ from importlib.util import find_spec if find_spec('bidscoin') is None: sys.path.append(str(Path(__file__).parents[1])) -from bidscoin import templatefolder, pluginfolder, bidsmap_template, tutorialurl, trackusage, tracking, configfile, config, DEBUG +from bidscoin import templatefolder, pluginfolder, bidsmap_template, tutorialurl, trackusage, tracking, configdir, configfile, config, DEBUG yaml = YAML() yaml.representer.ignore_aliases = lambda *data: True # Expand aliases (https://stackoverflow.com/questions/58091449/disabling-alias-for-yaml-file-in-python) @@ -625,6 +625,19 @@ def reportcredits(args: list) -> None: LOGGER.info(f"No DueCredit citation files found in {Path(args[0]).resolve()} and {report.parent}") +def reset(delete: bool) -> None: + """ + Resets the configuration directory by deleting it if the `delete` parameter is set to True + + :param delete: If set to True, the configuration directory will be removed + :return: None + """ + + if not delete: return + + shutil.rmtree(configdir) + + def settracking(value: str) -> None: """ Set or show usage tracking @@ -677,6 +690,7 @@ def main(): pulltutorialdata(tutorialfolder=args.download) test_bidscoin(bidsmapfile=args.test) test_bidsmap(bidsmapfile=args.bidsmaptest) + reset(delete=args.reset) settracking(value=args.tracking) reportcredits(args=args.credits) diff --git a/bidscoin/bids.py b/bidscoin/bids.py index f78250ad..5b9452f9 100644 --- a/bidscoin/bids.py +++ b/bidscoin/bids.py @@ -1262,7 +1262,7 @@ def exist_run(self, runitem: RunItem, datatype: Union[str, DataType]='') -> bool return False - def get_matching_run(self, sourcefile: Union[str, Path], dataformat, runtime=False) -> tuple[RunItem, str]: + def get_matching_run(self, sourcefile: Union[str, Path], dataformat: str='', runtime: bool=False) -> tuple[RunItem, str]: """ Find the first run in the bidsmap with properties and attributes that match with the data source. Only non-empty properties and attributes are matched, except when runtime is True, then the empty attributes are also matched. @@ -1271,12 +1271,21 @@ def get_matching_run(self, sourcefile: Union[str, Path], dataformat, runtime=Fal ignoredatatypes (e.g. 'exclude') -> normal datatypes (e.g. 'anat') -> unknowndatatypes (e.g. 'extra_data') :param sourcefile: The full filepath of the data source for which to get a run-item - :param dataformat: The dataformat section in the bidsmap in which a matching run is searched for, e.g. 'DICOM' + :param dataformat: The dataformat section in the bidsmap in which a matching run is searched for, e.g. 'DICOM'. Leave empty to recursively search through all dataformats :param runtime: Dynamic <> are expanded if True :return: (run, provenance) A vanilla run that has all its attributes populated with the source file attributes. If there is a match, the provenance of the bidsmap entry is returned, otherwise it will be '' """ + # Iterate over all dataformats if dataformat is not given + if not dataformat: + runitem, provenance = RunItem(), '' + for dformat in self.dataformats: + runitem, provenance = self.get_matching_run(sourcefile, dformat.dataformat, runtime) + if provenance: break + return runitem, provenance + + # Defaults datasource = DataSource(sourcefile, self.plugins, dataformat, options=self.options) unknowndatatypes = self.options.get('unknowntypes') or ['unknown_data'] ignoredatatypes = self.options.get('ignoretypes') or [] @@ -1284,7 +1293,7 @@ def get_matching_run(self, sourcefile: Union[str, Path], dataformat, runtime=Fal rundata = {'provenance': str(sourcefile), 'properties': {}, 'attributes': {}, 'bids': {}, 'meta': {}, 'events': {}} """The a run-item data structure. NB: Keep in sync with the RunItem() data attributes""" - # Loop through all datatypes and runs; all info goes cleanly into runitem (to avoid formatting problem of the CommentedMap) + # Iterate over all datatypes and runs; all info goes cleanly into runitem (to avoid formatting problem of the CommentedMap) if 'fmap' in normaldatatypes: normaldatatypes.insert(0, normaldatatypes.pop(normaldatatypes.index('fmap'))) # Put fmap at the front (to catch inverted polarity scans first for datatype in ignoredatatypes + normaldatatypes + unknowndatatypes: # The ordered datatypes in which a matching run is searched for diff --git a/bidscoin/bidscoiner.py b/bidscoin/bidscoiner.py index e9f6e0ad..65ba4c45 100755 --- a/bidscoin/bidscoiner.py +++ b/bidscoin/bidscoiner.py @@ -101,7 +101,7 @@ def bidscoiner(sourcefolder: str, bidsfolder: str, participant: list=(), force: # Load the data conversion plugins plugins = [plugin for name in bidsmap.plugins if (plugin := bcoin.import_plugin(name))] if not plugins: - LOGGER.warning(f"The plugins listed in your bidsmap['Options'] did not have a usable `bidscoiner_plugin` function, nothing to do") + LOGGER.warning(f"The {bidsmap.plugins.keys()} plugins listed in your bidsmap['Options'] did not have a usable `bidscoiner` interface, nothing to do") LOGGER.info('-------------- FINISHED! ------------') LOGGER.info('') return diff --git a/bidscoin/bidseditor.py b/bidscoin/bidseditor.py index b80e9d3d..c7467e71 100755 --- a/bidscoin/bidseditor.py +++ b/bidscoin/bidseditor.py @@ -24,6 +24,7 @@ sys.path.append(str(Path(__file__).parents[1])) from bidscoin import bcoin, bids, bidsversion, check_version, trackusage, bidsmap_template, __version__ from bidscoin.bids import BidsMap, RunItem, DataType +from bidscoin.utilities import is_dicomfile, is_parfile config.INVALID_KEY_BEHAVIOR = 'IGNORE' ROW_HEIGHT = 22 @@ -958,7 +959,7 @@ def open_inspectwindow(self, index: int): datafile = Path(self.filesystem.fileInfo(index).absoluteFilePath()) if datafile.is_file(): ext = ''.join(datafile.suffixes).lower() - if bids.is_dicomfile(datafile) or bids.is_parfile(datafile) or ext in sum((klass.valid_exts for klass in nib.imageclasses.all_image_classes), ('.nii.gz',)): + if is_dicomfile(datafile) or is_parfile(datafile) or ext in sum((klass.valid_exts for klass in nib.imageclasses.all_image_classes), ('.nii.gz',)): self.popup = InspectWindow(datafile) self.popup.show() self.popup.scrollbar.setValue(0) # This can only be done after self.popup.show() @@ -2041,11 +2042,11 @@ def __init__(self, filename: Path): super().__init__() ext = ''.join(filename.suffixes).lower() - if bids.is_dicomfile(filename): + if is_dicomfile(filename): if filename.name == 'DICOMDIR': LOGGER.bcdebug(f"Getting DICOM fields from {filename} will raise dcmread error below if pydicom => v3.0") text = str(dcmread(filename, force=True)) - elif bids.is_parfile(filename) or ext in ('.spar', '.txt', '.text', '.log'): + elif is_parfile(filename) or ext in ('.spar', '.txt', '.text', '.log'): text = filename.read_text() elif ext == '.7': try: diff --git a/bidscoin/bidsmapper.py b/bidscoin/bidsmapper.py index cbd6aa7b..02540753 100755 --- a/bidscoin/bidsmapper.py +++ b/bidscoin/bidsmapper.py @@ -103,7 +103,7 @@ def bidsmapper(sourcefolder: str, bidsfolder: str, bidsmap: str, template: str, # Import the data scanning plugins plugins = [plugin for name in bidsmap_new.plugins if (plugin := bcoin.import_plugin(name))] if not plugins: - LOGGER.warning(f"The plugins listed in your bidsmap['Options'] did not have a usable `bidsmapper_plugin` function, nothing to do") + LOGGER.warning(f"The {bidsmap_new.plugins.keys()} plugins listed in your bidsmap['Options'] did not have a usable `bidsmapper` interface, nothing to do") LOGGER.info('-------------- FINISHED! ------------') LOGGER.info('') return bidsmap_new diff --git a/bidscoin/cli/_bcoin.py b/bidscoin/cli/_bcoin.py index 9a20fd8f..f5ce3691 100755 --- a/bidscoin/cli/_bcoin.py +++ b/bidscoin/cli/_bcoin.py @@ -54,6 +54,7 @@ def get_parser() -> argparse.ArgumentParser: parser.add_argument('-t', '--test', help='Test the bidscoin installation and template bidsmap', nargs='?', metavar='TEMPLATE', const=bidsmap_template) parser.add_argument('-b', '--bidsmaptest', help='Test the run-items and their bidsnames of all normal runs in the study bidsmap. Provide the bids-folder or the bidsmap filepath', metavar='BIDSMAP') parser.add_argument('-c', '--credits', help='Show duecredit citations for your BIDS repository. You can also add duecredit summary arguments (without dashes), e.g. `style {apa,harvard1}` or `format {text,bibtex}`.', metavar='OPTIONS', nargs='+') + parser.add_argument('-r', '--reset', help='Restore the settings, plugins and template bidsmaps in your home directory to their default values', action='store_true') parser.add_argument( '--tracking', help='Show the usage tracking info {show}, or set usage tracking to {yes} or {no}', choices=['yes','no','show']) parser.add_argument('-v', '--version', help='Show the installed version and check for updates', action='version', version=f"BIDS-version:\t\t{bidsversion()}\nBIDScoin-version:\t{__version__}, {versionmessage}") diff --git a/bidscoin/cli/_dicomsort.py b/bidscoin/cli/_dicomsort.py index 5df893a7..d2199d65 100755 --- a/bidscoin/cli/_dicomsort.py +++ b/bidscoin/cli/_dicomsort.py @@ -22,7 +22,7 @@ class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescri ' dicomsort raw/sub-011/ses-mri01\n' ' dicomsort raw --subprefix sub- --sesprefix ses-\n' ' dicomsort myproject/raw/DICOMDIR --subprefix pat^ --sesprefix\n' - ' dicomsort sub-011/ses-mri01/DICOMDIR -n {AcquisitionNumber:05d}_{InstanceNumber:05d}.dcm\n ') + " dicomsort sub-011/ses-mri01/DICOMDIR -n '{AcquisitionNumber:05d}_{InstanceNumber:05d}.dcm'\n ") parser.add_argument('sourcefolder', help='The root folder containing the [sub/][ses/] dicomfiles or the DICOMDIR file') parser.add_argument('-i','--subprefix', help='Provide a prefix string to recursively sort sourcefolder/subject subfolders (e.g. "sub-" or "S_")', metavar='PREFIX') parser.add_argument('-j','--sesprefix', help='Provide a prefix string to recursively sort sourcefolder/subject/session subfolders (e.g. "ses-" or "T_")', metavar='PREFIX') diff --git a/bidscoin/cli/_rawmapper.py b/bidscoin/cli/_rawmapper.py index cae48c8a..ce10bcde 100755 --- a/bidscoin/cli/_rawmapper.py +++ b/bidscoin/cli/_rawmapper.py @@ -29,7 +29,7 @@ class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescri parser.add_argument('sourcefolder', help='The source folder with the raw data in sub-#/ses-#/series organization', metavar='FOLDER') parser.add_argument('-s','--sessions', help='Space separated list of selected sub-#/ses-# names/folders to be processed. Otherwise all sessions in the bidsfolder will be processed', nargs='+', metavar='SESSION') parser.add_argument('-f','--field', help='The fieldname(s) of the DICOM attribute(s) used to rename or map the subid/sesid foldernames', default=['PatientComments', 'ImageComments'], nargs='+', metavar='NAME') - parser.add_argument('-w','--wildcard', help='The Unix style pathname pattern expansion that is used to select the series from which the dicomfield is being mapped (can contain wildcards)', default='*', metavar='PATTERN') + parser.add_argument('-w','--wildcard', help='The Unix style pathname pattern expansion that is used to select the series folders from which the dicomfield is being mapped (can contain wildcards)', default='*', metavar='PATTERN') parser.add_argument('-o','--outfolder', help='The mapper-file is normally saved in sourcefolder or, when using this option, in outfolder', metavar='FOLDER') parser.add_argument('-r','--rename', help='Rename sub-subid/ses-sesid directories in the sourcefolder to sub-dcmval/ses-dcmval', action='store_true') parser.add_argument('-c','--clobber', help='Rename the sub/ses directories, even if the target-directory already exists', action='store_true') diff --git a/bidscoin/plugins/__init__.py b/bidscoin/plugins/__init__.py index 379d2c8d..e5394a8e 100644 --- a/bidscoin/plugins/__init__.py +++ b/bidscoin/plugins/__init__.py @@ -63,7 +63,10 @@ def bidsmapper(self, session: Path, bidsmap_new: 'BidsMap', bidsmap_old: 'BidsMa """ # See for every source file in the session if we already discovered it or not - for sourcefile in session.rglob('*'): + sourcefiles = session.rglob('*') + if not sourcefiles: + LOGGER.info(f"No {__name__} sourcedata found in: {session}") + for sourcefile in sourcefiles: # Check if the sourcefile is of a supported dataformat if is_hidden(sourcefile.relative_to(session)) or not (dataformat := self.has_support(sourcefile, dataformat='')): diff --git a/bidscoin/plugins/dcm2niix2bids.py b/bidscoin/plugins/dcm2niix2bids.py index 92d167a5..c28f5773 100644 --- a/bidscoin/plugins/dcm2niix2bids.py +++ b/bidscoin/plugins/dcm2niix2bids.py @@ -140,6 +140,8 @@ def bidsmapper(self, session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap, LOGGER.error(f"Unsupported dataformat '{dataformat}'") # See for every data source in the session if we already discovered it or not + if not sourcefiles: + LOGGER.info(f"No {__name__} sourcedata found in: {session}") for sourcefile in sourcefiles: # Check if the source files all have approximately the same size (difference < 50kB) diff --git a/bidscoin/utilities/dicomsort.py b/bidscoin/utilities/dicomsort.py index 8b2a71af..ad1ec8f5 100755 --- a/bidscoin/utilities/dicomsort.py +++ b/bidscoin/utilities/dicomsort.py @@ -41,7 +41,6 @@ def construct_name(scheme: str, dicomfile: Path, force: bool) -> str: value = int(value.replace('.','')) # Convert the SeriesInstanceUID to an int if not value and value != 0 and not force: LOGGER.error(f"Missing '{field}' DICOM field specified in the '{scheme}' folder/naming scheme, cannot find a safe name for: {dicomfile}\n") - trackusage('dicomsort_error') return '' else: schemevalues[field] = value @@ -113,7 +112,6 @@ def sortsession(sessionfolder: Path, dicomfiles: list[Path], folderscheme: str, subfolder = construct_name(folderscheme, dicomfile, force) if not subfolder: LOGGER.error('Cannot create subfolders, aborting dicomsort()...') - trackusage('dicomsort_error') return destination = sessionfolder/subfolder if not destination.is_dir(): diff --git a/docs/installation.rst b/docs/installation.rst index b6310bf3..2ecceff9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -61,6 +61,7 @@ Run your pip install command as before with the additional ``--upgrade`` or ``-- .. caution:: - The bidsmaps are not guaranteed to be compatible between different BIDScoin versions - After a successful BIDScoin installation or upgrade, it may be needed to (re)do any adjustments that were done on your `template bidsmap <./bidsmap.html#building-your-own-template-bidsmap>`__ + - The code on GitHub does not always have a unique version number. Therefore, if you install the latest code from github, and then later re-install a newer BIDScoin with the same version number (e.g. the stable version from PyPi), then you need to actively delete your old user configuration. You can do this most easily by running ``bidscoin --reset`` Dcm2niix installation --------------------- diff --git a/docs/utilities.rst b/docs/utilities.rst index 681a0bc8..46e6f24b 100644 --- a/docs/utilities.rst +++ b/docs/utilities.rst @@ -110,7 +110,7 @@ The ``dicomsort`` command-line tool is a utility to move your flat- or DICOMDIR- dicomsort raw/sub-011/ses-mri01 dicomsort raw --subprefix sub- --sesprefix ses- dicomsort myproject/raw/DICOMDIR --subprefix pat^ --sesprefix - dicomsort sub-011/ses-mri01/DICOMDIR -n {AcquisitionNumber:05d}_{InstanceNumber:05d}.dcm + dicomsort sub-011/ses-mri01/DICOMDIR -n '{AcquisitionNumber:05d}_{InstanceNumber:05d}.dcm' rawmapper --------- @@ -140,8 +140,8 @@ Another command-line utility that can be helpful in organizing your source data subid/sesid foldernames (default: ['PatientComments', 'ImageComments']) -w PATTERN, --wildcard PATTERN The Unix style pathname pattern expansion that is used to select the series - from which the dicomfield is being mapped (can contain wildcards) (default: - *) + folders from which the dicomfield is being mapped (can contain wildcards) + (default: *) -o FOLDER, --outfolder FOLDER The mapper-file is normally saved in sourcefolder or, when using this option, in outfolder (default: None)