diff --git a/bidscoin/__init__.py b/bidscoin/__init__.py
index d382bd4c..c88c6df6 100644
--- a/bidscoin/__init__.py
+++ b/bidscoin/__init__.py
@@ -24,9 +24,11 @@
import shutil
import warnings
import tempfile
+import subprocess
from pathlib import Path
from importlib import metadata
from typing import Tuple, Union, List
+from logging import getLogger
from .due import due, Doi
try:
import tomllib
@@ -40,6 +42,8 @@
with open(Path(__file__).parents[1]/'pyproject.toml', 'rb') as fid:
__version__ = tomllib.load(fid)['project']['version']
+LOGGER = getLogger(__name__)
+
# Add license metadata
__license__ = 'GNU General Public License v3.0 or later (GPLv3+)'
__copyright__ = f"2018-{datetime.date.today().year}, Marcel Zwiers"
@@ -142,6 +146,25 @@ def lsdirs(folder: Path, wildcard: str='*') -> List[Path]:
return sorted([item for item in sorted(folder.glob(wildcard)) if item.is_dir() and not is_hidden(item.relative_to(folder))])
+def run_command(command: str, success: tuple=(0,None)) -> int:
+ """
+ Runs a command in a shell using subprocess.run(command, ..)
+
+ :param command: The command that is executed
+ :param success: The return codes for successful operation (e,g, for dcm2niix it is (0,3))
+ :return: The return code (e.g. 0 if the command was successfully executed (no errors), > 0 otherwise)
+ """
+
+ LOGGER.verbose(f"Command:\n{command}")
+ process = subprocess.run(command, shell=True, capture_output=True, text=True)
+ if process.stderr or process.returncode not in success:
+ LOGGER.error(f"Failed to run:\n{command}\nErrorcode {process.returncode}:\n{process.stdout}\n{process.stderr}")
+ else:
+ LOGGER.verbose(f"Output:\n{process.stdout}")
+
+ return process.returncode
+
+
def trackusage(event: str, dryrun: bool=False) -> dict:
"""Sends a url GET request with usage data parameters (if tracking is allowed and we are not asleep)
diff --git a/bidscoin/bcoin.py b/bidscoin/bcoin.py
index 20484714..cc55d7de 100755
--- a/bidscoin/bcoin.py
+++ b/bidscoin/bcoin.py
@@ -8,10 +8,8 @@
import os
import types
import coloredlogs
-import inspect
import logging
import shutil
-import subprocess
import sys
import urllib.request
import time
@@ -221,25 +219,6 @@ def reporterrors() -> str:
return errors
-def run_command(command: str, success: tuple=(0,None)) -> int:
- """
- Runs a command in a shell using subprocess.run(command, ..)
-
- :param command: The command that is executed
- :param success: The return codes for successful operation (e,g, for dcm2niix it is (0,3))
- :return: The return code (e.g. 0 if the command was successfully executed (no errors), > 0 otherwise)
- """
-
- LOGGER.verbose(f"Command:\n{command}")
- process = subprocess.run(command, shell=True, capture_output=True, text=True)
- if process.stderr or process.returncode not in success:
- LOGGER.error(f"Failed to run:\n{command}\nErrorcode {process.returncode}:\n{process.stdout}\n{process.stderr}")
- else:
- LOGGER.verbose(f"Output:\n{process.stdout}")
-
- return process.returncode
-
-
def list_executables(show: bool=False) -> list:
"""
:param show: Print the installed console scripts if True
@@ -454,8 +433,7 @@ def test_plugin(plugin: Union[Path,str], options: dict) -> int:
# First test to see if we can import the plugin
module = import_plugin(plugin, ('bidsmapper_plugin','bidscoiner_plugin'))
- if not inspect.ismodule(module):
- LOGGER.error(f"Invalid plugin: '{plugin}'")
+ if module is None:
return 1
# Then run the plugin's own 'test' routine (if implemented)
diff --git a/bidscoin/bids.py b/bidscoin/bids.py
index 1d930b6c..aa913e21 100644
--- a/bidscoin/bids.py
+++ b/bidscoin/bids.py
@@ -2530,7 +2530,7 @@ def addmetadata(bidsses: Path):
json.dump(jsondata, sidecar, indent=4)
-def updatemetadata(datasource: DataSource, targetmeta: Path, usermeta: Meta, extensions: Iterable, sourcemeta: Path=Path()) -> Meta:
+def poolmetadata(datasource: DataSource, targetmeta: Path, usermeta: Meta, extensions: Iterable, sourcemeta: Path=Path()) -> Meta:
"""
Load the metadata from the target (json sidecar), then add metadata from the source (json sidecar) and finally add
the user metadata (meta table). Source metadata other than json sidecars are copied over to the target folder. Special
diff --git a/bidscoin/bidsapps/fixmeta.py b/bidscoin/bidsapps/fixmeta.py
index cb127961..69170099 100755
--- a/bidscoin/bidsapps/fixmeta.py
+++ b/bidscoin/bidsapps/fixmeta.py
@@ -87,7 +87,7 @@ def fixmeta(bidsfolder: str, pattern: str, metadata: dict, participant: list, bi
# Load/copy over the source meta-data
jsonfile = target.with_suffix('').with_suffix('.json')
- jsondata = bids.updatemetadata(datasource, jsonfile, bids.Meta({}), ['.json'])
+ jsondata = bids.poolmetadata(datasource, jsonfile, bids.Meta({}), ['.json'])
for key, value in metadata.items():
if isinstance(value, list):
for n in range(0, len(value), 2):
diff --git a/bidscoin/bidsapps/skullstrip.py b/bidscoin/bidsapps/skullstrip.py
index 419b7b34..985436de 100755
--- a/bidscoin/bidsapps/skullstrip.py
+++ b/bidscoin/bidsapps/skullstrip.py
@@ -20,7 +20,7 @@
from importlib.util import find_spec
if find_spec('bidscoin') is None:
sys.path.append(str(Path(__file__).parents[2]))
-from bidscoin import bcoin, bids, lsdirs, trackusage, DEBUG
+from bidscoin import bcoin, bids, lsdirs, trackusage, run_command, DEBUG
from bidscoin.due import due, Doi
@@ -141,7 +141,7 @@ def skullstrip(bidsfolder: str, pattern: str, participant: list, masked: str, ou
jt.outputPath = f"{os.getenv('HOSTNAME')}:{Path.cwd() if DEBUG else tempfile.gettempdir()}/{jt.jobName}.out"
jobids.append(pbatch.runJob(jt))
LOGGER.info(f"Your skullstrip job has been submitted with ID: {jobids[-1]}")
- elif bcoin.run_command(f"mri_synthstrip -i {srcimg} -o {outputimg} -m {maskimg} {args}"):
+ elif run_command(f"mri_synthstrip -i {srcimg} -o {outputimg} -m {maskimg} {args}"):
continue
# Add a json sidecar-file with the "SkullStripped" field
diff --git a/bidscoin/plugins/README b/bidscoin/plugins/README
deleted file mode 100644
index 2a60481e..00000000
--- a/bidscoin/plugins/README
+++ /dev/null
@@ -1,175 +0,0 @@
-"""This plugin contains placeholder code demonstrating the bidscoin plugin API, both for the bidsmapper and for
-the bidscoiner. The functions in this module are called if the basename of this module (when located in the
-plugins-folder; otherwise the full path must be provided) is listed in the bidsmap. The following plugin functions
-are expected to be present:
-
-- test: A test function for the plugin + its bidsmap options. Can be called by the user from the bidseditor and the bidscoin utility
-- has_support: A function to assess whether a source file is supported by the plugin. The return value should correspond to a data format section in the bidsmap
-- get_attribute: A function to read an attribute value from a source file
-- bidsmapper_plugin: A function to discover BIDS-mappings in a source data session
-- bidscoiner_plugin: A function to convert a single source data session to bids according to the specified BIDS-mappings
-
-To avoid code duplications and minimize plugin development time, various support functions are available in
-BIDScoin's library modules named 'bcoin' and, most notably, 'bids'"""
-
-import logging
-from pathlib import Path
-from bidscoin.due import due, Doi
-from bidscoin.bids import Bidsmap, Run, Plugin
-
-LOGGER = logging.getLogger(__name__)
-
-# The default options that are set when installing the plugin
-OPTIONS = Plugin({'command': 'demo', # Plugin option
- 'args': 'foo bar'}) # Another plugin option
-
-# The default bids-mappings that are added when installing the plugin
-BIDSMAP = {'DemoFormat':{
- 'subject': '<>', # This filesystem property extracts the subject label from the source directory. NB: Any property or attribute can be used, e.g.
- 'session': '<>', # This filesystem property extracts the session label from the source directory. NB: Any property or attribute can be used, e.g.
-
- 'func': [ # ----------------------- All functional runs --------------------
- {'provenance': '', # The fullpath name of the source file from which the attributes and properties are read. Serves also as a look-up key to find a run in the bidsmap
- 'properties': # The matching (regex) criteria go in here
- {'filepath': '', # File folder, e.g. ".*Parkinson.*" or ".*(phantom|bottle).*"
- 'filename': '', # File name, e.g. ".*fmap.*" or ".*(fmap|field.?map|B0.?map).*"
- 'filesize': '', # File size, e.g. "2[4-6]\d MB" for matching files between 240-269 MB
- 'nrfiles': ''}, # Number of files in the folder that match the above criteria, e.g. "5/d/d" for matching a number between 500-599
- 'attributes': # The matching (regex) criteria go in here
- {'ch_num': '.*',
- 'filetype': '.*',
- 'freq': '.*',
- 'ch_name': '.*',
- 'units': '.*',
- 'trigger_idx': '.*'},
- 'bids':
- {'task': '',
- 'acq': '',
- 'ce': '',
- 'dir': '',
- 'rec': '',
- 'run': '<<>>', # This will be updated during bidscoiner runtime (as it depends on the already existing files)
- 'recording': '',
- 'suffix': 'physio'},
- 'meta': # This is an optional entry for meta-data dictionary that are appended to the json sidecar files
- {'TriggerChannel': '<>',
- 'ExpectedTimepoints': '<>',
- 'ChannelNames': '<>',
- 'Threshold': '<>',
- 'TimeOffset': '<>'}}],
-
- 'exclude': [ # ----------------------- Data that will be left out -------------
- {'provenance': '',
- 'properties':
- {'filepath': '',
- 'filename': '',
- 'filesize': '',
- 'nrfiles': ''},
- 'attributes':
- {'ch_num': '.*',
- 'filetype': '.*',
- 'freq': '.*',
- 'ch_name': '.*',
- 'units': '.*',
- 'trigger_idx': '.*'},
- 'bids':
- {'task': '',
- 'acq': '',
- 'ce': '',
- 'dir': '',
- 'rec': '',
- 'run': '<<>>',
- 'recording': '',
- 'suffix': 'physio'},
- 'meta':
- {'TriggerChannel': '<>',
- 'ExpectedTimepoints': '<>',
- 'ChannelNames': '<>',
- 'Threshold': '<>',
- 'TimeOffset': '<>'}}]}}
-
-
-def test(options: Plugin=OPTIONS) -> int:
- """
- Performs a runtime/integration test of the working of the plugin + its bidsmap options
-
- :param options: A dictionary with the plugin options, e.g. taken from the bidsmap['Options']['plugins']['README']
- :return: The errorcode (e.g 0 if the tool generated the expected result, > 0 if there was a tool error)
- """
-
- LOGGER.info(f'This is a demo-plugin test routine, validating its working with options: {options}')
-
- return 0
-
-
-def has_support(file: Path) -> str:
- """
- This plugin function assesses whether a sourcefile is of a supported dataformat
-
- :param file: The sourcefile that is assessed
- :return: The valid/supported dataformat of the sourcefile
- """
-
- if file.is_file():
-
- LOGGER.verbose(f'This is a demo-plugin has_support routine, assessing whether "{file}" has a valid dataformat')
- return 'dataformat' if file == 'supportedformat' else ''
-
- return ''
-
-
-def get_attribute(dataformat: str, sourcefile: Path, attribute: str, options: Plugin) -> str:
- """
- This plugin function reads attributes from the supported sourcefile
-
- :param dataformat: The dataformat of the sourcefile, e.g. DICOM of PAR
- :param sourcefile: The sourcefile from which key-value data needs to be read
- :param attribute: The attribute key for which the value needs to be retrieved
- :param options: A dictionary with the plugin options, e.g. taken from the bidsmap['Options']['plugins']
- :return: The retrieved attribute value
- """
-
- if dataformat in ('DICOM','PAR'):
- LOGGER.verbose(f'This is a demo-plugin get_attribute routine, reading the {dataformat} "{attribute}" attribute value from "{sourcefile}"')
-
- return ''
-
-
-def bidsmapper_plugin(session: Path, bidsmap_new: Bidsmap, bidsmap_old: Bidsmap, template: Bidsmap, store: dict) -> None:
- """
- All the logic to map the data source properties and attributes onto bids labels go into this plugin function. The function is
- expected to update/append new runs to the bidsmap_new data structure. The bidsmap options for this plugin can
- be found in:
-
- bidsmap_new/old.plugins['README']
-
- See also the dcm2niix2bids plugin for reference implementation
-
- :param session: The full-path name of the subject/session raw data source folder
- :param bidsmap_new: The new study bidsmap that we are building
- :param bidsmap_old: The previous study bidsmap that has precedence over the template bidsmap
- :param template: The template bidsmap with the default heuristics
- :param store: The paths of the source- and target-folder
- :return:
- """
-
- LOGGER.verbose(f'This is a bidsmapper demo-plugin working on: {session}')
-
-
-@due.dcite(Doi('put.your/doi.here'), description='This is an optional duecredit decorator for citing your paper(s)', tags=['implementation'])
-def bidscoiner_plugin(session: Path, bidsmap: Bidsmap, bidsses: Path) -> Union[None, dict]:
- """
- The plugin to convert the runs in the source folder and save them in the bids folder. Each saved datafile should be
- accompanied by a json sidecar file. The bidsmap options for this plugin can be found in:
-
- bidsmap_new/old.plugins['README']
-
- See also the dcm2niix2bids plugin for reference implementation
-
- :param session: The full-path name of the subject/session raw data source folder
- :param bidsmap: The full mapping heuristics from the bidsmap YAML-file
- :param bidsses: The full-path name of the BIDS output `sub-/ses-` folder
- :return: A dictionary with personal data for the participants.tsv file (such as sex or age)
- """
-
- LOGGER.verbose(f'This is a bidscoiner demo-plugin working on: {session} -> {bidsses}')
diff --git a/bidscoin/plugins/dcm2niix2bids.py b/bidscoin/plugins/dcm2niix2bids.py
index 26c493b2..a5615540 100644
--- a/bidscoin/plugins/dcm2niix2bids.py
+++ b/bidscoin/plugins/dcm2niix2bids.py
@@ -13,7 +13,7 @@
from bids_validator import BIDSValidator
from typing import Union, List
from pathlib import Path
-from bidscoin import bcoin, bids, lsdirs, due, Doi
+from bidscoin import bcoin, bids, run_command, lsdirs, due, Doi
from bidscoin.utilities import physio
from bidscoin.bids import BidsMap, DataFormat, Plugin, Plugins
try:
@@ -56,7 +56,7 @@ def test(options: Plugin=OPTIONS) -> int:
LOGGER.warning(f"The expected 'args' key is not defined in the dcm2niix2bids options")
# Test the dcm2niix installation
- errorcode = bcoin.run_command(f"{options.get('command', OPTIONS['command'])} -v", (0,3))
+ errorcode = run_command(f"{options.get('command', OPTIONS['command'])} -v", (0,3))
# Test reading an attribute from a PAR-file
parfile = Path(data_path)/'phantom_EPI_asc_CLEAR_2_1.PAR'
@@ -98,8 +98,8 @@ def get_attribute(dataformat: Union[DataFormat, str], sourcefile: Path, attribut
:param dataformat: The bidsmap-dataformat of the sourcefile, e.g. DICOM of PAR
:param sourcefile: The sourcefile from which the attribute value should be read
:param attribute: The attribute key for which the value should be read
- :param options: A dictionary with the plugin options, e.g. taken from the bidsmap.plugins
- :return: The attribute value
+ :param options: A dictionary with the plugin options, e.g. taken from the bidsmap.plugins['dcm2niix2bids']
+ :return: The retrieved attribute value
"""
if dataformat == 'DICOM':
return bids.get_dicomfield(attribute, sourcefile)
@@ -110,13 +110,13 @@ def get_attribute(dataformat: Union[DataFormat, str], sourcefile: Path, attribut
def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap, template: BidsMap) -> None:
"""
- All the logic to map the DICOM/PAR source fields onto bids labels go into this function
+ The goal of this plugin function is to identify all the different runs in the session and update the
+ bidsmap if a new run is discovered
:param session: The full-path name of the subject/session raw data source folder
:param bidsmap_new: The new study bidsmap that we are building
:param bidsmap_old: The previous study bidsmap that has precedence over the template bidsmap
:param template: The template bidsmap with the default heuristics
- :return:
"""
# Get started
@@ -139,7 +139,7 @@ def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap,
else:
LOGGER.error(f"Unsupported dataformat '{dataformat}'")
- # Update the bidsmap with the info from the source files
+ # See for every data source in the session if we already discovered it or not
for sourcefile in sourcefiles:
# Check if the source files all have approximately the same size (difference < 50kB)
@@ -150,18 +150,18 @@ def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap,
break
# See if we can find a matching run in the old bidsmap
- run, match = bidsmap_old.get_matching_run(sourcefile, dataformat)
+ run, oldmatch = bidsmap_old.get_matching_run(sourcefile, dataformat)
# If not, see if we can find a matching run in the template
- if not match:
+ if not oldmatch:
LOGGER.bcdebug('No match found in the study bidsmap, now trying the template bidsmap')
run, _ = template.get_matching_run(sourcefile, dataformat)
- # See if we have collected the run somewhere in our new bidsmap
+ # See if we have already put the run somewhere in our new bidsmap
if not bidsmap_new.exist_run(run):
# Communicate with the user if the run was not present in bidsmap_old or in template, i.e. that we found a new sample
- if not match:
+ if not oldmatch:
LOGGER.info(f"Discovered sample: {run.datasource}")
@@ -192,8 +192,10 @@ def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap,
@due.dcite(Doi('10.1016/j.jneumeth.2016.03.001'), description='dcm2niix: DICOM to NIfTI converter', tags=['reference-implementation'])
def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[None, dict]:
"""
- The bidscoiner plugin to convert the session DICOM and PAR/REC source-files into BIDS-valid NIfTI-files in the
- corresponding bids session-folder and extract personals (e.g. Age, Sex) from the source header
+ The bidscoiner plugin to convert the session DICOM and PAR/REC source-files into BIDS-valid NIfTI-files in the corresponding
+ bids session-folder and extract personals (e.g. Age, Sex) from the source header. The bidsmap options for this plugin can be found in:
+
+ bidsmap.plugins['spec2nii2bids']
:param session: The full-path name of the subject/session source folder
:param bidsmap: The full mapping heuristics from the bidsmap YAML-file
@@ -254,13 +256,13 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[N
# Check if we should ignore this run
if run.datatype in bidsmap.options['ignoretypes']:
LOGGER.info(f"--> Leaving out: {run.datasource}")
- bids.bidsprov(bidsses, source, run) # Write out empty provenance data
+ bids.bidsprov(bidsses, source, run) # Write out empty provenance logging data
continue
# Check if we already know this run
if not runid:
LOGGER.error(f"--> Skipping unknown run: {run.datasource}\n-> Re-run the bidsmapper and delete {bidsses} to solve this warning")
- bids.bidsprov(bidsses, source) # Write out empty provenance data
+ bids.bidsprov(bidsses, source) # Write out empty provenance logging data
continue
LOGGER.info(f"--> Coining: {run.datasource}")
@@ -321,7 +323,7 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[N
filename = bidsname,
outfolder = outfolder,
source = source)
- if bcoin.run_command(command) and not next(outfolder.glob(f"{bidsname}*"), None):
+ if run_command(command) and not next(outfolder.glob(f"{bidsname}*"), None):
continue
# Collect the bidsname
@@ -464,7 +466,7 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[N
jsonfile = target.with_suffix('').with_suffix('.json')
if not jsonfile.is_file():
LOGGER.warning(f"Unexpected conversion result, could not find: {jsonfile}")
- metadata = bids.updatemetadata(run.datasource, jsonfile, run.meta, options.get('meta',[]))
+ metadata = bids.poolmetadata(run.datasource, jsonfile, run.meta, options.get('meta',[]))
# Remove the bval/bvec files of sbref- and inv-images (produced by dcm2niix but not allowed by the BIDS specifications)
if ((run.datatype == 'dwi' and suffix == 'sbref') or
diff --git a/bidscoin/plugins/nibabel2bids.py b/bidscoin/plugins/nibabel2bids.py
index b6ed488e..6d158df7 100644
--- a/bidscoin/plugins/nibabel2bids.py
+++ b/bidscoin/plugins/nibabel2bids.py
@@ -86,8 +86,8 @@ def get_attribute(dataformat: Union[DataFormat, str], sourcefile: Path, attribut
:param dataformat: The bidsmap-dataformat of the sourcefile, e.g. DICOM of PAR
:param sourcefile: The sourcefile from which the attribute value should be read
:param attribute: The attribute key for which the value should be read
- :param options: A dictionary with the plugin options, e.g. taken from the bidsmap.plugins
- :return: The attribute value
+ :param options: A dictionary with the plugin options, e.g. taken from the bidsmap.plugins['nibabel2bids']
+ :return: The retrieved attribute value
"""
value = None
@@ -107,16 +107,16 @@ def get_attribute(dataformat: Union[DataFormat, str], sourcefile: Path, attribut
def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap, template: BidsMap) -> None:
"""
- All the logic to map the Nibabel header fields onto bids labels go into this function
+ The goal of this plugin function is to identify all the different runs in the session and update the
+ bidsmap if a new run is discovered
:param session: The full-path name of the subject/session raw data source folder
:param bidsmap_new: The new study bidsmap that we are building
:param bidsmap_old: The previous study bidsmap that has precedence over the template bidsmap
:param template: The template bidsmap with the default heuristics
- :return:
"""
- # Collect the different DICOM/PAR source files for all runs in the session
+ # See for every source file in the session if we already discovered it or not
for sourcefile in session.rglob('*'):
# Check if the sourcefile is of a supported dataformat
@@ -124,17 +124,17 @@ def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap,
continue
# See if we can find a matching run in the old bidsmap
- run, match = bidsmap_old.get_matching_run(sourcefile, dataformat)
+ run, oldmatch = bidsmap_old.get_matching_run(sourcefile, dataformat)
# If not, see if we can find a matching run in the template
- if not match:
+ if not oldmatch:
run, _ = template.get_matching_run(sourcefile, dataformat)
- # See if we have collected the run somewhere in our new bidsmap
+ # See if we have already put the run somewhere in our new bidsmap
if not bidsmap_new.exist_run(run):
# Communicate with the user if the run was not present in bidsmap_old or in template, i.e. that we found a new sample
- if not match:
+ if not oldmatch:
LOGGER.info(f"Discovered sample: {run.datasource}")
else:
LOGGER.bcdebug(f"Known sample: {run.datasource}")
@@ -154,7 +154,7 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> None:
:param session: The full-path name of the subject/session source folder
:param bidsmap: The full mapping heuristics from the bidsmap YAML-file
:param bidsses: The full-path name of the BIDS output `sub-/ses-` folder
- :return: Nothing
+ :return: Nothing (i.e. personal data is not available)
"""
# Get the subject identifiers from the bidsses folder
@@ -184,13 +184,13 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> None:
# Check if we should ignore this run
if run.datatype in bidsmap.options['ignoretypes']:
LOGGER.info(f"--> Leaving out: {run.datasource}")
- bids.bidsprov(bidsses, sourcefile, run) # Write out empty provenance data
+ bids.bidsprov(bidsses, sourcefile, run) # Write out empty provenance loggin data
continue
# Check if we already know this run
if not runid:
LOGGER.error(f"Skipping unknown run: {run.datasource}\n-> Re-run the bidsmapper and delete {bidsses} to solve this warning")
- bids.bidsprov(bidsses, sourcefile) # Write out empty provenance data
+ bids.bidsprov(bidsses, sourcefile) # Write out empty provenance logging data
continue
LOGGER.info(f"--> Coining: {run.datasource}")
@@ -217,7 +217,7 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> None:
LOGGER.warning(f"{target} already exists and will be deleted -- check your results carefully!")
target.unlink()
- # Save the sourcefile as a BIDS NIfTI file and write out provenance data
+ # Save the sourcefile as a BIDS NIfTI file and write out provenance logging data
nib.save(nib.load(sourcefile), target)
bids.bidsprov(bidsses, sourcefile, run, [target] if target.is_file() else [])
@@ -228,7 +228,7 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> None:
# Load/copy over the source meta-data
sidecar = target.with_suffix('').with_suffix('.json')
- metadata = bids.updatemetadata(run.datasource, sidecar, run.meta, options.get('meta', []))
+ metadata = bids.poolmetadata(run.datasource, sidecar, run.meta, options.get('meta', []))
if metadata:
with sidecar.open('w') as json_fid:
json.dump(metadata, json_fid, indent=4)
diff --git a/bidscoin/plugins/spec2nii2bids.py b/bidscoin/plugins/spec2nii2bids.py
index 65e54e26..0b0c39e6 100644
--- a/bidscoin/plugins/spec2nii2bids.py
+++ b/bidscoin/plugins/spec2nii2bids.py
@@ -10,7 +10,7 @@
from typing import Union
from bids_validator import BIDSValidator
from pathlib import Path
-from bidscoin import bcoin, bids, due, Doi
+from bidscoin import run_command, bids, due, Doi
from bidscoin.bids import BidsMap, DataFormat, Plugin
LOGGER = logging.getLogger(__name__)
@@ -25,7 +25,7 @@
def test(options: Plugin=OPTIONS) -> int:
"""
- This plugin shell tests the working of the spec2nii2bids plugin + its bidsmap options
+ This plugin shell tests the working of the spec2nii2bids plugin + given options
:param options: A dictionary with the plugin options, e.g. taken from the bidsmap.plugins['spec2nii2bids']
:return: The errorcode (e.g 0 if the tool generated the expected result, > 0 if there was a tool error)
@@ -43,7 +43,7 @@ def test(options: Plugin=OPTIONS) -> int:
LOGGER.warning(f"The expected 'args' key is not defined in the spec2nii2bids options")
# Test the spec2nii installation
- return bcoin.run_command(f"{options.get('command',OPTIONS['command'])} -v")
+ return run_command(f"{options.get('command',OPTIONS['command'])} -v")
def has_support(file: Path, dataformat: Union[DataFormat, str]='') -> str:
@@ -76,7 +76,7 @@ def get_attribute(dataformat: Union[DataFormat, str], sourcefile: Path, attribut
:param dataformat: The dataformat of the sourcefile, e.g. DICOM of PAR
:param sourcefile: The sourcefile from which key-value data needs to be read
:param attribute: The attribute key for which the value needs to be retrieved
- :param options: The bidsmap['Options']['spec2nii2bids'] dictionary with the plugin options
+ :param options: The bidsmap.plugins['spec2nii2bids'] dictionary with the plugin options
:return: The retrieved attribute value
"""
@@ -104,20 +104,16 @@ def get_attribute(dataformat: Union[DataFormat, str], sourcefile: Path, attribut
def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap, template: BidsMap) -> None:
"""
- All the heuristics spec2nii2bids attributes and properties onto bids labels and meta-data go into this plugin function.
- The function is expected to update/append new runs to the bidsmap_new data structure. The bidsmap options for this plugin
- are stored in:
-
- bidsmap_new['Options']['plugins']['spec2nii2bids']
+ The goal of this plugin function is to identify all the different runs in the session and update the
+ bidsmap if a new run is discovered
:param session: The full-path name of the subject/session raw data source folder
:param bidsmap_new: The new study bidsmap that we are building
:param bidsmap_old: The previous study bidsmap that has precedence over the template bidsmap
:param template: The template bidsmap with the default heuristics
- :return:
"""
- # Update the bidsmap with the info from the source files
+ # See for every source file in the session if we already discovered it or not
for sourcefile in session.rglob('*'):
# Check if the sourcefile is of a supported dataformat
@@ -125,17 +121,17 @@ def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap,
continue
# See if we can find a matching run in the old bidsmap
- run, match = bidsmap_old.get_matching_run(sourcefile, dataformat)
+ run, oldmatch = bidsmap_old.get_matching_run(sourcefile, dataformat)
# If not, see if we can find a matching run in the template
- if not match:
+ if not oldmatch:
run, _ = template.get_matching_run(sourcefile, dataformat)
- # See if we have collected the run somewhere in our new bidsmap
+ # See if we have already put the run somewhere in our new bidsmap
if not bidsmap_new.exist_run(run):
# Communicate with the user if the run was not present in bidsmap_old or in template, i.e. that we found a new sample
- if not match:
+ if not oldmatch:
LOGGER.info(f"Discovered sample: {run.datasource}")
else:
LOGGER.bcdebug(f"Known sample: {run.datasource}")
@@ -153,7 +149,7 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[N
This wrapper function around spec2nii converts the MRS data in the session folder and saves it in the bidsfolder.
Each saved datafile should be accompanied by a json sidecar file. The bidsmap options for this plugin can be found in:
- bidsmap_new['Options']['plugins']['spec2nii2bids']
+ bidsmap.plugins['spec2nii2bids']
:param session: The full-path name of the subject/session raw data source folder
:param bidsmap: The full mapping heuristics from the bidsmap YAML-file
@@ -188,13 +184,13 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[N
# Check if we should ignore this run
if run.datatype in bidsmap.options['ignoretypes']:
LOGGER.info(f"--> Leaving out: {run.datasource}")
- bids.bidsprov(bidsses, sourcefile, run) # Write out empty provenance data
+ bids.bidsprov(bidsses, sourcefile, run) # Write out empty provenance logging data
continue
# Check that we know this run
if not runid:
LOGGER.error(f"Skipping unknown run: {run.datasource}\n-> Re-run the bidsmapper and delete the MRS output data in {bidsses} to solve this warning")
- bids.bidsprov(bidsses, sourcefile) # Write out empty provenance data
+ bids.bidsprov(bidsses, sourcefile) # Write out empty provenance logging data
continue
LOGGER.info(f"--> Coining: {run.datasource}")
@@ -237,7 +233,7 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[N
LOGGER.error(f"Unsupported dataformat: {dataformat}")
return
command = options.get('command', 'spec2nii')
- errcode = bcoin.run_command(f'{command} {dformat} -j -f "{bidsname}" -o "{outfolder}" {args} {arg} "{sourcefile}"')
+ errcode = run_command(f'{command} {dformat} -j -f "{bidsname}" -o "{outfolder}" {args} {arg} "{sourcefile}"')
bids.bidsprov(bidsses, sourcefile, run, [target] if target.is_file() else [])
if not target.is_file():
if not errcode:
@@ -246,7 +242,7 @@ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[N
# Load/copy over and adapt the newly produced json sidecar-file
sidecar = target.with_suffix('').with_suffix('.json')
- metadata = bids.updatemetadata(run.datasource, sidecar, run.meta, options.get('meta',[]))
+ metadata = bids.poolmetadata(run.datasource, sidecar, run.meta, options.get('meta',[]))
if metadata:
with sidecar.open('w') as json_fid:
json.dump(metadata, json_fid, indent=4)
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 04e87c22..ba492c6a 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -4,15 +4,15 @@
## [dev]
-## [4.4.0] - 2024
+## [4.4.0] - 2024-10-02
### Added
-- Support for BIDS v1.10.0 (including MRS)
+- Support for BIDS v1.10.0 (including MRS data)
### Changed
-- BIDScoin's main API, which now includes new classes to separate the logic from the data and make the code cleaner, better organized and maintainable
+- BIDScoin's main API, which now includes new classes to separate the logic from the bidsmap data and make the code cleaner, better organized and easier to maintain
- Dropped support for Qt5
-- Plugins are now installed in the BIDScoin configuration directory
+- Plugins are now installed in BIDScoin's user configuration directory
## [4.3.3] - 2024-07-12
@@ -463,7 +463,8 @@ A first stable release of BIDScoin :-)
### To do
- Add support for non-imaging data
-[dev]: https://github.com/Donders-Institute/bidscoin/compare/4.3.3...HEAD
+[dev]: https://github.com/Donders-Institute/bidscoin/compare/4.4.0...HEAD
+[4.3.4]: https://github.com/Donders-Institute/bidscoin/compare/4.3.3...4.4.0
[4.3.3]: https://github.com/Donders-Institute/bidscoin/compare/4.3.2...4.3.3
[4.3.2]: https://github.com/Donders-Institute/bidscoin/compare/4.3.1...4.3.2
[4.3.1]: https://github.com/Donders-Institute/bidscoin/compare/4.3.0...4.3.1
diff --git a/docs/_static/dictionary-custom.txt b/docs/_static/dictionary-custom.txt
index 37865859..c9a2e436 100644
--- a/docs/_static/dictionary-custom.txt
+++ b/docs/_static/dictionary-custom.txt
@@ -163,6 +163,7 @@ copymetadata
csf
dataflow
dataformat
+dataformats
dataset
datasets
datasource
@@ -275,6 +276,8 @@ rebranded
refactoring
regex
runindex
+runitem
+RunItem
runtime
sbatch
screenshot
diff --git a/docs/plugins.rst b/docs/plugins.rst
index d3220585..b85ef678 100644
--- a/docs/plugins.rst
+++ b/docs/plugins.rst
@@ -30,193 +30,216 @@ The nibabel2bids plugin wraps around the versatile `nibabel >', # This filesystem property extracts the subject label from the source directory. NB: Any property or attribute can be used, e.g.
- 'session': '<>', # This filesystem property extracts the session label from the source directory. NB: Any property or attribute can be used, e.g.
-
- 'func': [ # ----------------------- All functional runs --------------------
- {'provenance': '', # The fullpath name of the source file from which the attributes and properties are read. Serves also as a look-up key to find a run in the bidsmap
- 'properties': # The matching (regex) criteria go in here
- {'filepath': '', # File folder, e.g. ".*Parkinson.*" or ".*(phantom|bottle).*"
- 'filename': '', # File name, e.g. ".*fmap.*" or ".*(fmap|field.?map|B0.?map).*"
- 'filesize': '', # File size, e.g. "2[4-6]\d MB" for matching files between 240-269 MB
- 'nrfiles': ''}, # Number of files in the folder that match the above criteria, e.g. "5/d/d" for matching a number between 500-599
- 'attributes': # The matching (regex) criteria go in here
- {'ch_num': '.*',
- 'filetype': '.*',
- 'freq': '.*',
- 'ch_name': '.*',
- 'units': '.*',
- 'trigger_idx': '.*'},
- 'bids':
- {'task': '',
- 'acq': '',
- 'ce': '',
- 'dir': '',
- 'rec': '',
- 'run': '<<>>', # This will be updated during bidscoiner runtime (as it depends on the already existing files)
- 'recording': '',
- 'suffix': 'physio'},
- 'meta': # This is an optional entry for meta-data dictionary that are appended to the json sidecar files
- {'TriggerChannel': '<>',
- 'ExpectedTimepoints': '<>',
- 'ChannelNames': '<>',
- 'Threshold': '<>',
- 'TimeOffset': '<>'}}],
-
- [...]
-
- 'exclude': [ # ----------------------- Data that will be left out -------------
- {'provenance': '',
- 'properties':
- {'filepath': '',
- 'filename': '',
- 'filesize': '',
- 'nrfiles': ''},
- 'attributes':
- {'ch_num': '.*',
- 'filetype': '.*',
- 'freq': '.*',
- 'ch_name': '.*',
- 'units': '.*',
- 'trigger_idx': '.*'},
- 'bids':
- {'task': '',
- 'acq': '',
- 'ce': '',
- 'dir': '',
- 'rec': '',
- 'run': '<<>>',
- 'recording': '',
- 'suffix': 'physio'},
- 'meta':
- {'TriggerChannel': '<>',
- 'ExpectedTimepoints': '<>',
- 'ChannelNames': '<>',
- 'Threshold': '<>',
- 'TimeOffset': '<>'}}]}}
-
-
- def test(options: dict=OPTIONS) -> bool:
- """
- Performs a runtime/integration test of the working of the plugin + its bidsmap options
-
- :param options: A dictionary with the plugin options, e.g. taken from the bidsmap['Options']['plugins']['README']
- :return: The errorcode (e.g 0 if the tool generated the expected result, > 0 if there was a tool error)
- """
-
- LOGGER.info(f'This is a demo-plugin test routine, validating its working with options: {options}')
-
- return 0
-
-
- def has_support(file: Path) -> str:
- """
- This plugin function assesses whether a sourcefile is of a supported dataformat
-
- :param file: The sourcefile that is assessed
- :return: The valid/supported dataformat of the sourcefile
- """
-
- if file.is_file():
-
- LOGGER.verbose(f'This is a demo-plugin has_support routine, assessing whether "{file}" has a valid dataformat')
- return 'dataformat' if file == 'supportedformat' else ''
+ import logging
+ from pathlib import Path
+ from bidscoin.due import due, Doi
+ from bidscoin.bids import BidsMap
+
+ LOGGER = logging.getLogger(__name__)
+
+ # The default options that are set when installing the plugin. This is optional and acts as a fallback
+ # in case the plugin options are not specified in the (template) bidsmap
+ OPTIONS = {'command': 'demo', # Plugin option
+ 'args': 'foo bar'} # Another plugin option
+
+ # The default bids-mappings that are added when installing the plugin. This is optional and only acts
+ # as a fallback in case the dataformat section is not present in the bidsmap. So far, this feature is
+ # not used by any of the plugins
+ BIDSMAP = {'DemoFormat':{
+ 'subject': '<>', # This filesystem property extracts the subject label from the source directory. NB: Any property or attribute can be used, e.g.
+ 'session': '<>', # This filesystem property extracts the session label from the source directory. NB: Any property or attribute can be used, e.g.
+
+ 'func': [ # ----------------------- All functional runs --------------------
+ {'provenance': '', # The fullpath name of the source file from which the attributes and properties are read. Serves also as a look-up key to find a run in the bidsmap
+ 'properties': # The matching (regex) criteria go in here
+ {'filepath': '', # File folder, e.g. ".*Parkinson.*" or ".*(phantom|bottle).*"
+ 'filename': '', # File name, e.g. ".*fmap.*" or ".*(fmap|field.?map|B0.?map).*"
+ 'filesize': '', # File size, e.g. "2[4-6]\d MB" for matching files between 240-269 MB
+ 'nrfiles': ''}, # Number of files in the folder that match the above criteria, e.g. "5/d/d" for matching a number between 500-599
+ 'attributes': # The matching (regex) criteria go in here
+ {'ch_num': '.*',
+ 'filetype': '.*',
+ 'freq': '.*',
+ 'ch_name': '.*',
+ 'units': '.*',
+ 'trigger_idx': '.*'},
+ 'bids':
+ {'task': '',
+ 'acq': '',
+ 'ce': '',
+ 'dir': '',
+ 'rec': '',
+ 'run': '<<>>', # This will be updated during bidscoiner runtime (as it depends on the already existing files)
+ 'recording': '',
+ 'suffix': 'physio'},
+ 'meta': # This is an optional entry for meta-data dictionary that are appended to the json sidecar files
+ {'TriggerChannel': '<>',
+ 'TimeOffset': '<>'}}],
+
+ 'exclude': [ # ----------------------- Data that will be left out -------------
+ {'attributes':
+ {'ch_num': '.*',
+ 'filetype': '.*',
+ 'freq': '.*',
+ 'ch_name': '.*',
+ 'units': '.*',
+ 'trigger_idx': '.*'},
+ 'bids':
+ {'task': '',
+ 'acq': '',
+ 'ce': '',
+ 'dir': '',
+ 'rec': '',
+ 'run': '<<>>',
+ 'recording': '',
+ 'suffix': 'physio'}
+
+
+ def test(options: dict=OPTIONS) -> int:
+ """
+ Performs a runtime/integration test of the working of this plugin + given options
+
+ :param options: A dictionary with the plugin options, e.g. taken from `bidsmap.plugins[__name__]`
+ :return: The errorcode (e.g 0 if the tool generated the expected result, > 0 if there was
+ a tool error)
+ """
+
+ LOGGER.info(f'This is a demo-plugin test routine, validating its working with options: {options}')
+
+ return 0
+
+
+ def has_support(file: Path) -> str:
+ """
+ This plugin function assesses whether a sourcefile is of a supported dataformat
+
+ :param file: The sourcefile that is assessed
+ :param dataformat: The requested dataformat (optional requirement)
+ :return: The name of the supported dataformat of the sourcefile. This name should
+ correspond to the name of a dataformat in the bidsmap
+ """
+
+ if file.is_file():
+
+ LOGGER.verbose(f'This has_support routine assesses whether "{file}" is of a known dataformat')
+ return 'dataformat_name' if file == 'of_a_supported_format' else ''
return ''
- def get_attribute(dataformat: str, sourcefile: Path, attribute: str, options: dict) -> str:
- """
- This plugin function reads attributes from the supported sourcefile
+ def get_attribute(dataformat: str, sourcefile: Path, attribute: str, options: dict) -> str:
+ """
+ This plugin function reads attributes from the supported sourcefile
- :param dataformat: The bidsmap-dataformat of the sourcefile, e.g. DICOM of PAR
- :param sourcefile: The sourcefile from which the attribute value should be read
- :param attribute: The attribute key for which the value should be read
- :param options: A dictionary with the plugin options, e.g. taken from the bidsmap['Options']
- :return: The attribute value
- """
+ :param dataformat: The dataformat of the sourcefile, e.g. DICOM of PAR
+ :param sourcefile: The sourcefile from which key-value data needs to be read
+ :param attribute: The attribute key for which the value needs to be retrieved
+ :param options: A dictionary with the plugin options, e.g. taken from the bidsmap.plugins[__name__]
+ :return: The retrieved attribute value
+ """
- if dataformat in ('DICOM','PAR'):
- LOGGER.verbose(f'This is a demo-plugin get_attribute routine, reading the {dataformat} "{attribute}" attribute value from "{sourcefile}"')
+ if dataformat in ('DICOM','PAR'):
+ LOGGER.verbose(f'This is a demo-plugin get_attribute routine, reading the {dataformat} "{attribute}" attribute value from "{sourcefile}"')
+ return read(sourcefile, attribute)
- return ''
+ return ''
+
+
+ def bidsmapper_plugin(session: Path, bidsmap_new: BidsMap, bidsmap_old: BidsMap, template: BidsMap) -> None:
+ """
+ The goal of this plugin function is to identify all the different runs in the session and update the
+ bidsmap if a new run is discovered
+
+ :param session: The full-path name of the subject/session raw data source folder
+ :param bidsmap_new: The new study bidsmap that we are building
+ :param bidsmap_old: The previous study bidsmap that has precedence over the template bidsmap
+ :param template: The template bidsmap with the default heuristics
+ """
+
+ # See for every data source in the session if we already discovered it or not
+ for sourcefile in session.rglob('*'):
+
+ # Check if the sourcefile is of a supported dataformat
+ if not (dataformat := has_support(sourcefile)):
+ continue
+
+ # See if we can find a matching run in the old bidsmap
+ run, oldmatch = bidsmap_old.get_matching_run(sourcefile, dataformat)
+
+ # If not, see if we can find a matching run in the template
+ if not oldmatch:
+ run, _ = template.get_matching_run(sourcefile, dataformat)
+
+ # See if we have already put the run somewhere in our new bidsmap
+ if not bidsmap_new.exist_run(run):
+
+ # Communicate with the user if the run was not present in bidsmap_old or in template, i.e. that we found a new sample
+ if not oldmatch:
+ LOGGER.info(f"Discovered sample: {run.datasource}")
+
+ # Do some stuff with the run if needed
+ pass
+
+ # Copy the filled-in run over to the new bidsmap
+ bidsmap_new.insert_run(run)
- def bidsmapper_plugin(session: Path, bidsmap_new: dict, bidsmap_old: dict, template: dict, store: dict) -> None:
- """
- All the logic to map the Philips PAR/REC fields onto bids labels go into this plugin function. The function is
- expected to update / append new runs to the bidsmap_new data structure. The bidsmap options for this plugin can
- be found in:
+ @due.dcite(Doi('put.your/doi.here'), description='This is an optional duecredit decorator for citing your paper(s)', tags=['implementation'])
+ def bidscoiner_plugin(session: Path, bidsmap: BidsMap, bidsses: Path) -> Union[None, dict]:
+ """
+ The plugin to convert the runs in the source folder and save them in the bids folder. Each saved datafile should be
+ accompanied by a json sidecar file. The bidsmap options for this plugin can be found in:
- bidsmap_new/old['Options']['plugins']['README']
+ bidsmap.plugins[__name__]
- See also the dcm2niix2bids plugin for reference implementation
+ See also the dcm2niix2bids plugin for reference implementation
- :param session: The full-path name of the subject/session raw data source folder
- :param bidsmap_new: The new study bidsmap that we are building
- :param bidsmap_old: The previous study bidsmap that has precedence over the template bidsmap
- :param template: The template bidsmap with the default heuristics
- :param store: The paths of the source- and target-folder
- :return:
- """
+ :param session: The full-path name of the subject/session raw data source folder
+ :param bidsmap: The full mapping heuristics from the bidsmap YAML-file
+ :param bidsses: The full-path name of the BIDS output `sub-/ses-` folder
+ :return: A dictionary with personal data for the participants.tsv file (such as sex or age)
+ """
- LOGGER.verbose(f'This is a bidsmapper demo-plugin working on: {session}')
+ # Go over the different source files in the session
+ for sourcefile in session.rglob('*'):
+ # Check if the sourcefile is of a supported dataformat
+ if not (dataformat := has_support(sourcefile)):
+ continue
- def bidscoiner_plugin(session: Path, bidsmap: dict, bidsses: Path) -> Union[None, dict]:
- """
- The plugin to convert the runs in the source folder and save them in the bids folder. Each saved datafile should be
- accompanied by a json sidecar file. The bidsmap options for this plugin can be found in:
+ # Get a matching run from the bidsmap
+ run, runid = bidsmap.get_matching_run(sourcefile, dataformat, runtime=True)
- bidsmap_new/old['Options']['plugins']['README']
+ # Compose the BIDS filename using the matched run
+ bidsname = run.bidsname(subid, sesid, validkeys=True, runtime=True)
- See also the dcm2niix2bids plugin for reference implementation
+ # Save the sourcefile as a BIDS NIfTI file
+ targetfile = (outfolder/bidsname).with_suffix('.nii')
+ convert(sourcefile, targetfile)
- :param session: The full-path name of the subject/session source folder
- :param bidsmap: The full mapping heuristics from the bidsmap YAML-file
- :param bidsses: The full-path name of the BIDS output 'ses-' folder
- :return: A dictionary with personal data for the participants.tsv file (such as sex or age)
- """
+ # Write out provenance logging data (= useful but not strictly necessary)
+ bids.bidsprov(bidsses, sourcefile, run, targetfile)
- LOGGER.bcdebug(f'This is a bidscoiner demo-plugin working on: {session} -> {bidsfolder}')
+ # Pool all sources of meta-data and save it as a json sidecar file
+ sidecar = targetfile.with_suffix('.json')
+ ext_meta = bidsmap.plugins[__name__]['meta']
+ metadata = bids.poolmetadata(run.datasource, sidecar, run.meta, ext_meta)
+ save(sidecar, metadata)
-*The README plugin placeholder code*
+*Plugin placeholder code, illustrating the structure of a plugin with minimal functionality*
diff --git a/docs/utilities.rst b/docs/utilities.rst
index c45273e9..681a0bc8 100644
--- a/docs/utilities.rst
+++ b/docs/utilities.rst
@@ -17,8 +17,8 @@ The bidscoin command-line utility serves as a central starting point to test and
$ bidsmapper sourcefolder bidsfolder # This produces a study bidsmap and launches a GUI
$ bidscoiner sourcefolder bidsfolder # This converts your data to BIDS according to the study bidsmap
- Default settings and template bidsmaps are stored in the `.bidscoin` configuration folder in your home
- directory (you can modify the configuration files to your needs with any plain text editor)
+ Default settings, plugins and template bidsmaps are stored in the `.bidscoin` configuration folder in your
+ home directory (you can modify the configuration files to your needs with any plain text editor)
Set the environment variable `BIDSCOIN_DEBUG=TRUE` to run BIDScoin in a more verbose logging mode and
`BIDSCOIN_CONFIGDIR=/writable/path/to/configdir` for using a different configuration (root) directory.
diff --git a/tests/test_bcoin.py b/tests/test_bcoin.py
index b1af9576..332a4517 100644
--- a/tests/test_bcoin.py
+++ b/tests/test_bcoin.py
@@ -23,7 +23,7 @@ def test_bidsversion():
def test_runcommand():
- assert bcoin.run_command('bidscoin') == 0
+ assert bidscoin.run_command('bidscoin') == 0
def test_list_executables():
@@ -32,7 +32,7 @@ def test_list_executables():
assert 'dicomsort' in executables
assert 'deface' in executables
for executable in executables:
- assert bcoin.run_command(f"{executable} -h") == 0
+ assert bidscoin.run_command(f"{executable} -h") == 0
if not sys.platform.startswith('win'):
manpage = (Path(sys.executable).parents[1]/'share'/'man'/'man1'/f"{executable}.1").read_text()
assert executable in manpage.splitlines() # Tests if manpage NAME == argparse prog for each console script
diff --git a/tests/test_bids.py b/tests/test_bids.py
index 7a9e0670..5fa88508 100644
--- a/tests/test_bids.py
+++ b/tests/test_bids.py
@@ -680,7 +680,7 @@ def test_updatemetadata(dcm_file, tmp_path):
'B0FieldIdentifier': ['Identifier<>', 'Identifier']})
# Test if the user metadata takes precedence
- metadata = bids.updatemetadata(extdatasource, sidecar, usermeta, ['.json'])
+ metadata = bids.poolmetadata(extdatasource, sidecar, usermeta, ['.json'])
assert metadata['PatientName'] == 'UserTest'
assert metadata['DynamicName'] == 'CompressedSamples^MR1'
assert metadata['B0FieldSource'] == 'Source<>'
@@ -688,10 +688,10 @@ def test_updatemetadata(dcm_file, tmp_path):
assert not (outfolder/sourcefile.with_suffix('.jsn').name).is_file()
# Test if the source metadata takes precedence
- metadata = bids.updatemetadata(extdatasource, sidecar, Meta({}), ['.jsn', '.json'], sourcefile)
+ metadata = bids.poolmetadata(extdatasource, sidecar, Meta({}), ['.jsn', '.json'], sourcefile)
assert metadata['PatientName'] == 'SourceTest'
assert (outfolder/sourcefile.with_suffix('.jsn').name).is_file()
# Test if the sidecar metadata takes precedence
- metadata = bids.updatemetadata(extdatasource, sidecar, Meta({}), [])
+ metadata = bids.poolmetadata(extdatasource, sidecar, Meta({}), [])
assert metadata['PatientName'] == 'SidecarTest'
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 980e72d2..4b7f4771 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -24,3 +24,8 @@ def test_plugin(plugin, options):
# Then run the plugin's own 'test' routine (if implemented)
assert module.test(options.get(plugin.stem, {})) == 0
+
+ # Test that we don't import invalid plugins
+ module = bcoin.import_plugin(plugin, ('foo_plugin', 'bar_plugin'))
+ if module is not None:
+ raise ImportError(f"Unintended plugin import: '{plugin}'")