Source code for sdcflows.utils.bimap

# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
#
# Copyright 2021 The NiPreps Developers <nipreps@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# We support and encourage derived works from this project, please read
# about our expectations at
#
#     https://www.nipreps.org/community/licensing/
#
"""A bidirectional hashmap."""
import re

_autokey_pat = re.compile(r"^auto_(\d+)$")


[docs] class bidict(dict): """ A bidirectional hashmap. >>> d = bidict({"a": 1, "b": 2}, c=3) >>> d["a"] 1 >>> d[1] 'a' >>> 2 in d True >>> "b" in d True >>> d["d"] = 4 >>> del d["d"] >>> d["d"] = 4 >>> del d[4] >>> d {'a': 1, 'b': 2, 'c': 3} >>> d["d"] = "d" # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: 'd' <> 'd' is a self-mapping >>> d["d"] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): KeyError: 'd' >>> d["b"] = None # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): KeyError: 'b' is already in mapping >>> d[1] = None # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): KeyError: '1' is already a value in mapping >>> d["d"] = 1 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ValueError: '1' is already in mapping >>> d["d"] = "a" # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ValueError: 'a' is already a key in mapping >>> d["unhashable val"] = [] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: value '[]' of unhashable type: 'list' >>> d[list()] = 1 # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: key '[]' of unhashable type: 'list' >>> d.add("a new value") 'auto_00000' >>> d["auto_00000"] 'a new value' >>> d["auto_00001"] = "another value" >>> d.add("a new value") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ValueError: 'a new value' is already in mapping >>> d.add("third value") 'auto_00002' >>> d == bidict(reversed(list(d.items()))) True >>> bidict({"a": 1, "b": 1}) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): TypeError: Bidirectional dictionary cannot contain repeated values >>> del d["e"] # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): KeyError: 'e' >>> list(d) ['a', 'b', 'c', 'auto_00000', 'auto_00001', 'auto_00002'] >>> list(d.values()) [1, 2, 3, 'a new value', 'another value', 'third value'] >>> d.clear() >>> d {} """ _inverse = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._inverse = {v: k for k, v in self.items()} if len(self) != len(self._inverse): raise TypeError("Bidirectional dictionary cannot contain repeated values") def __setitem__(self, key, value): if key == value: raise TypeError(f"'{key}' <> '{value}' is a self-mapping") try: hash(value) except TypeError as exc: raise TypeError(f"value '{value}' of {exc}") try: hash(key) except TypeError as exc: raise TypeError(f"key '{key}' of {exc}") if self.__contains__(key): raise KeyError( f"'{key}' is already {'a value' * (key in self._inverse)} in mapping" ) if self.__contains__(value): raise ValueError( f"'{value}' is already {'a key' * (value not in self._inverse)} in mapping" ) super().__setitem__(key, value) self._inverse[value] = key def __delitem__(self, key): if not self.__contains__(key): raise KeyError(f"'{key}") if super().__contains__(key): del self._inverse[super().__getitem__(key)] super().__delitem__(key) else: super().__delitem__(self._inverse[key]) del self._inverse[key] def __getitem__(self, key): if key in self._inverse: return self._inverse[key] return super().__getitem__(key) def __contains__(self, key): if super().__contains__(key): return True return key in self._inverse
[docs] def add(self, value): """Insert a new value in the bidict, generating an automatic key.""" _used = set( int(i.groups()[0]) for i in [ _autokey_pat.match(k) for k in self.keys() if k.startswith("auto_") ] if i is not None ) for i in range(len(_used) + 1): if i not in _used: newkey = f"auto_{i:05d}" self.__setitem__(newkey, value) return newkey
[docs] def clear(self): """Empty of all key/value pairs.""" self._inverse.clear() super().clear()
[docs] class EstimatorRegistry(bidict): """ A specialized :py:class:`bidict` to track :py:class:`~sdcflows.fieldmaps.FieldmapEstimation`. Examples -------- >>> estimators = EstimatorRegistry() >>> _ = estimators.add(("file3.txt", "file4.txt")) >>> estimators.sources ['file3.txt', 'file4.txt'] >>> _ = estimators.add(("file1.txt", "file2.txt")) >>> estimators.sources ['file1.txt', 'file2.txt', 'file3.txt', 'file4.txt'] >>> _ = estimators.add(("file3.txt", "file2.txt")) >>> estimators.sources ['file1.txt', 'file2.txt', 'file3.txt', 'file4.txt'] >>> estimators.get_key("file3.txt") ('auto_00000', 'auto_00002') >>> estimators.get_key("file5.txt") () """ @property def sources(self): """Return a flattened list of fieldmap sources.""" return sorted(set([el for group in self.values() for el in group]))
[docs] def get_key(self, value): """Get the key(s) containing a particular value.""" if value not in self.sources: return tuple() return tuple(sorted(k for k, v in self.items() if value in v))