Skip to content

Commit

Permalink
Bugfix for refactoring of IO helper functions (bidscoin.plugins -> …
Browse files Browse the repository at this point in the history
…`bidscoin.utilities`) + minor tweaks
  • Loading branch information
marcelzwiers committed Jan 6, 2025
1 parent d5997bd commit a45d0d1
Show file tree
Hide file tree
Showing 13 changed files with 46 additions and 17 deletions.
16 changes: 15 additions & 1 deletion bidscoin/bcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
15 changes: 12 additions & 3 deletions bidscoin/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -1271,20 +1271,29 @@ 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 <<values>> 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 []
normaldatatypes = [dtype.datatype for dtype in self.dataformat(dataformat).datatypes if dtype not in unknowndatatypes + ignoredatatypes]
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
Expand Down
2 changes: 1 addition & 1 deletion bidscoin/bidscoiner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions bidscoin/bidseditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion bidscoin/bidsmapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions bidscoin/cli/_bcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down
2 changes: 1 addition & 1 deletion bidscoin/cli/_dicomsort.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion bidscoin/cli/_rawmapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
5 changes: 4 additions & 1 deletion bidscoin/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='')):
Expand Down
2 changes: 2 additions & 0 deletions bidscoin/plugins/dcm2niix2bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions bidscoin/utilities/dicomsort.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
1 change: 1 addition & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------------------
Expand Down
6 changes: 3 additions & 3 deletions docs/utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit a45d0d1

Please sign in to comment.