diff --git a/docs/bids_app/config.md b/docs/bids_app/config.md index c5bf7615..177766a2 100644 --- a/docs/bids_app/config.md +++ b/docs/bids_app/config.md @@ -26,10 +26,10 @@ The value of `filters` should be a dictionary where each key corresponds to a BI the bold component would match any paths under the `func/` datatype folder, with the suffix `bold` and the extension `.nii.gz`. ``` - sub-xxx/.../func/ent1-xxx_ent2-xxx_..._bold.nii.gz + sub-xxx/.../func/sub-xxx_ses-xxx_..._bold.nii.gz ``` -* [`boolean`](#bool): constrains presence or absence of the entity without restricting its value. `False` requires that the entity be **absent**, while `True` requires that the entity be **present**, regardless of value. +* [`boolean`](#bool): constrains presence or absence of the entity without restricting its value. `False` requires that the entity be **absent**, while `True` requires the entity to be **present**, regardless of value. ```yaml pybids_inputs: derivs: @@ -38,9 +38,12 @@ The value of `filters` should be a dictionary where each key corresponds to a BI desc: True acquisition: False ``` - The above example maps all paths in the `func/` datatype folder that have a `_desc-` entity but do not have the `_acq-` entity. + The above example selects all paths in the `func/` datatype folder that have a `_desc-` entity but do not have the `_acq-` entity. * [`list`](#list): Specify multiple string or boolean filters. Any path matching any one of the filters will be selected. Using `False` as one of the filters allows the entity to optionally be absent in addition to matching one of the string filters. Using `True` along with text is redundant, as `True` will cause any value to be selected. Using `True` with `False` is equivalent to not providing the filter at all. + + These filters: + ```yaml pybids_inputs: derivs: @@ -51,7 +54,19 @@ The value of `filters` should be a dictionary where each key corresponds to a BI - MP2RAGE ``` + would select all of the following paths: + + ``` + sub-001/ses-1/anat/sub-001_ses-001_acq-MPRAGE_run-1_T1w.nii.gz + sub-001/ses-1/anat/sub-001_ses-001_acq-MP2RAGE_run-1_T1w.nii.gz + sub-001/ses-1/anat/sub-001_ses-001_run-1_T1w.nii.gz + ``` + + * To use regex for filtering, use an additional subkey set either to [`match`](#re.match) or [`search`](#re.search), depending on which regex method you wish to use. This key may be set to any one of the above items (`str`, `bool`, or `list`). Only one such key may be used. + + These filters: + ```yaml pybids_inputs: derivs: @@ -62,8 +77,16 @@ The value of `filters` should be a dictionary where each key corresponds to a BI match: MP2?RAGE ``` + would select all of the following paths: + + ``` + sub-001/ses-1/anat/sub-001_ses-001_acq-MPRAGE_run-1_T1.nii.gz + sub-001/ses-1/anat/sub-001_ses-001_acq-MP2RAGE_run-1_t1w.nii.gz + sub-001/ses-1/anat/sub-001_ses-001_acq-MPRAGE_run-1_qT1w.nii.gz + ``` + ````{note} -`match` and `search` are both filtering methods. In addition to these, `get` is also a valid filtering method and may be used as the subkey for a filter. However, this is equivalent to directly providing the desired filter without a subkey: +`match` and `search` are both _filtering methods_. In addition to these, `get` is also a valid filtering method and may be used as the subkey for a filter. However, this is equivalent to directly providing the desired filter without a subkey: ```yaml pybids_inputs: diff --git a/snakebids/core/_querying.py b/snakebids/core/_querying.py index 7b89af31..0bc84c99 100644 --- a/snakebids/core/_querying.py +++ b/snakebids/core/_querying.py @@ -37,9 +37,9 @@ def add_filter( Converts a list of values to include or exclude into Pybids compatible filters. Exclusion filters are appropriately formatted as regex. Raises an exception if - both include and exclude are stipulated + both include and exclude are stipulated. - _Postfilter is modified in-place + PostFilter is modified in-place. Parameters ---------- @@ -47,10 +47,10 @@ def add_filter( Name of entity to be filtered inclusions Values to include, values not found in this list will be excluded, by - default None + default ``None`` exclusions Values to exclude, only values not found in this list will be included, by - default None + default ``None`` Raises ------ @@ -143,9 +143,9 @@ def prefilters(self) -> FilterMap: @ft.cached_property def get(self) -> CompiledFilter: - """The combination pre- and post- filters for indexing pybids via ``.get()``. + """The combination of pre- and post- filters for indexing pybids via ``.get()``. - Includes pre-filters not annotated for regex match or search and all inclusion + Includes pre-filters not annotated for regex querying and all inclusion post-filters. Empty post-filters are replaced with Query.ANY. This allows valid paths to be found and processed later. Post-filters are not applied when an equivalent prefilter is present @@ -171,7 +171,7 @@ def get(self) -> CompiledFilter: def search(self) -> CompiledFilter: """Pre-filters for indexing pybids via ``.get(regex_search=True)``. - As with :func:`_UnifiedFilter.get`, but only prefilters labelled for regex + As with :prop:`UnifiedFilter.get`, but only prefilters labelled for regex matching using ``search:`` or ``match:``. Raises @@ -251,10 +251,10 @@ def get_matching_files( "dataset." ) raise PybidsError(msg) from err - else: - if search is not None: - return [p for p in get if p in search] - return get + + if search is not None: + return [p for p in get if p in search] + return get @attrs.define @@ -279,8 +279,8 @@ class _TooFewKeysError(FilterSpecError): @override def get_config_error(self, component_name: str) -> ConfigError: msg = ( - f"Filter '{self.entity}' for component '{component_name}' was specified a " - f"dict but was not given any keys. {self.requirement} Got: {self.value}" + f"Filter '{self.entity}' for component '{component_name}' was specified as " + f"a dict but was not given any keys. {self.requirement} Got: {self.value}" ) return ConfigError(msg) @@ -352,6 +352,8 @@ def _compile_filt(filt: Iterable[str | bool]): result[key] = _compile_filt(itx.always_iterable(f)) if filt_type == "match": + # pybids only does search, so surround string filters with position anchors + # to simulate match result[key] = [ f"^(?:{filt})$" if isinstance(filt, str) else filt for filt in result[key] diff --git a/snakebids/core/input_generation.py b/snakebids/core/input_generation.py index c79f9742..ec541f3c 100644 --- a/snakebids/core/input_generation.py +++ b/snakebids/core/input_generation.py @@ -526,10 +526,10 @@ def _get_component( Parameters ---------- - bids_layout : BIDSLayout + bids_layout Layout from pybids for accessing the BIDS dataset to grab paths - pybids_inputs : dict + component Dictionary indexed by modality name, specifying the filters and wildcards for each pybids input. @@ -539,11 +539,6 @@ def _get_component( postfilters Filters to component after delineation - Yields - ------ - BidsComponent - One BidsComponent is yielded for each modality described by ``pybids_inputs``. - Raises ------ ConfigError @@ -554,12 +549,6 @@ def _get_component( filters = UnifiedFilter(component, postfilters or {}) if "custom_path" in component: - # a custom path was specified for this input, skip pybids: - # get input_wildcards by parsing path for {} entries (using a set - # to get unique only) - # get zip_lists by using glob_wildcards (but need to modify - # to deal with multiple wildcards - path = component["custom_path"] zip_lists = _parse_custom_path(path, filters=filters) return BidsComponent(name=input_name, path=path, zip_lists=zip_lists)