diff --git a/snakebids/core/datasets.py b/snakebids/core/datasets.py index 9d06bc1e..babd22f2 100644 --- a/snakebids/core/datasets.py +++ b/snakebids/core/datasets.py @@ -131,7 +131,7 @@ def expand( allow_missing=allow_missing if isinstance(allow_missing, bool) else list(itx.always_iterable(allow_missing)), - **{self.entity: list(set(self._data))}, + **{self.entity: list(dict.fromkeys(self._data))}, **{ wildcard: list(itx.always_iterable(v)) for wildcard, v in wildcards.items() @@ -389,7 +389,8 @@ def sequencify(item: bool | str | Iterable[str]) -> bool | list[str]: allow_missing_seq = sequencify(allow_missing) inner_expand = list( - set( + # order preserving deduplication + dict.fromkeys( sn_expand( list(itx.always_iterable(paths)), zip, diff --git a/snakebids/tests/test_datasets.py b/snakebids/tests/test_datasets.py index cffa1329..3ea115f4 100644 --- a/snakebids/tests/test_datasets.py +++ b/snakebids/tests/test_datasets.py @@ -335,6 +335,19 @@ def test_expand_deduplicates_paths(self, component: Expandable): paths = component.expand(path_tpl) assert len(paths) == len(set(paths)) + @given( + component=sb_st.expandables( + restrict_patterns=True, + path_safe=True, + unique=True, + ) + ) + def test_expand_preserves_entry_order(self, component: Expandable): + path_tpl = bids(**get_wildcard_dict(component.zip_lists)) + paths = component.expand(path_tpl) + for path, entity_vals in zip(paths, zip(*component.zip_lists.values())): + assert bids(**dict(zip(component.zip_lists.keys(), entity_vals))) == path + class TestBidsComponentExpand: """ diff --git a/typings/bids/layout/utils.pyi b/typings/bids/layout/utils.pyi index b496a3d5..b1aaa04f 100644 --- a/typings/bids/layout/utils.pyi +++ b/typings/bids/layout/utils.pyi @@ -2,7 +2,7 @@ This type stub file was generated by pyright. """ -"""Miscellaneous layout-related utilities.""" +from typing import Sequence class BIDSMetadata(dict): """Metadata dictionary that reports the associated file on lookup failures.""" @@ -66,8 +66,11 @@ class PaddedInt(int): def __hash__(self) -> int: ... def parse_file_entities( - filename, entities=..., config=..., include_unmatched=... -): # -> dict[Unknown, Unknown]: + filename: str, + entities: Sequence[str] = ..., + config: str = ..., + include_unmatched: bool = ..., +) -> dict[str, str]: """Parse the passed filename for entity/value pairs. Parameters