Source code for id_translation.transform._impl.bitmask

"""Transformations for translating bitmask fields."""

from collections.abc import Iterable, Mapping, MutableMapping
from itertools import filterfalse
from typing import TypedDict

from ..types import Transformer

IdType = int


class TomlOverrideRecord(TypedDict):
    id: IdType
    override: str


[docs] class BitmaskTransformer(Transformer[IdType]): r"""Transformations for translating bitmask fields. IDs must be integers. .. important:: When using TOML config, dict keys must be strings. Use alternative format for `overrides`: .. code-block:: toml [transform.'<source>'.BitmaskTransformer] overrides = [ { id = 0, override = "override-for-id=0" }, { id = 1, override = "override-for-id=1" }, ] Key names must match exactly, and IDs may not be repeated. For more information about TOML configuration, see :ref:`translator-config-transform`. Args: joiner: A string used to join bitmask flag labels. overrides: A dict ``{id: translation}``. Use to add or override the translation source. force_decomposition: If ``True``, ignore composite values in the translation source. force_real_translations: If ``True``, convert :class:`.MagicDict` instances to plain ``dict`` using the :attr:`.MagicDict.real` attribute. Results such as ``'<Failed: id=2> & 4:name-of-4'`` are possible when ``False``, and will be considered hits by :meth:`translate(max_fails \< 1) <.Translator.translate>` calls. Examples: Basic usage. >>> btr = BitmaskTransformer(overrides={0b000: "NOT_SET", 0b1000: "OVERFLOW!"}) Create a :class:`.Translator` using bitmask transforms for the `'bitmasks'` source. >>> from id_translation import Translator >>> data = {"id": [1, 4, 8], "name": ["name-of-1", "name-of-4", "0b1000"]} >>> tra = Translator({"bitmasks": data}, transformers={"bitmasks": btr}) Translate some bitmasks! >>> tra.translate((0b000, 0b101, 8), names="bitmasks") ('NOT_SET', '1:name-of-1 & 4:name-of-4', 'OVERFLOW!') Note that ``0='NOT_SET'`` was translated even though it's not in the ``data``, and that ``8='0b1000'`` was replaced by ``'OVERFLOW!'``, as per the overrides specified for the transformer. Implication of setting ``force_real_translations=False``. >>> btr = BitmaskTransformer(force_real_translations=False) >>> tra = Translator({"bitmasks": data}, transformers={"bitmasks": btr}) >>> tra.translate((5, 6), names="bitmasks", max_fails=0.0) ('1:name-of-1 & 4:name-of-4', '<Failed: id=2> & 4:name-of-4') The translation "succeeded", even though ``max_fails=0.0`` and ``6 = '<Failed: id=2> & 4:name-of-4'`` was only a partial success. This would've raised :class:`an error <.TooManyFailedTranslationsError>` if `force_real_translations` was not set. The transformer adds :attr:`~.MagicDict.real` mappings for all composite IDs, so the :class:`.Translator` won't detect any issues when using :meth:`.MagicDict.real_contains` to verify the results. """ def __init__( self, joiner: str = " & ", *, overrides: Mapping[IdType, str] | None = None, force_decomposition: bool = False, force_real_translations: bool = True, ) -> None: self._joiner = joiner self._force = force_decomposition if isinstance(overrides, list): # TOML keys must be strings, so we use a record format. overrides = self._from_toml_records(overrides) self._overrides = overrides or {} self._force_real_translations = force_real_translations
[docs] @classmethod def update_ids(cls, ids: set[IdType], /) -> None: """Add decomposed bitmask values.""" new_ids = set() for decomposed in map(cls.decompose_bitmask, ids): new_ids.update(decomposed) ids.update(new_ids)
[docs] def update_translations(self, translations: dict[IdType, str], /) -> None: """Join decomposed bitmask values using the `joiner` string.""" ids_to_update: Iterable[IdType] = filter(self.is_decomposable, translations) if not self._force: ids_to_update = filterfalse(translations.__contains__, ids_to_update) ids_to_update = list(ids_to_update) translations.update(self._overrides) new_translations = { bitmask: self._create_composite_translation(self.decompose_bitmask(bitmask), translations=translations) for bitmask in ids_to_update } translations.update(new_translations) translations.update(self._overrides)
[docs] def try_add_missing_key(self, key: IdType, /, *, translations: MutableMapping[IdType, str]) -> None: """Join decomposed bitmask values using the `joiner` string.""" bits = self.decompose_bitmask(key) if not bits: return try: translations[key] = self._create_composite_translation(bits, translations=translations) except KeyError: return
def _create_composite_translation(self, bits: list[IdType], *, translations: Mapping[IdType, str]) -> str: from id_translation.offline import MagicDict if self._force_real_translations and isinstance(translations, MagicDict): translations = translations.real return self._joiner.join(translations[idx] for idx in bits) def __repr__(self) -> str: overrides = self._overrides force_decomposition = self._force return f"{type(self).__name__}({self._joiner!r}, {overrides=}, {force_decomposition=})"
[docs] @classmethod def decompose_bitmask(cls, i: int, /) -> list[int]: """Decompose a bitmask into powers of two. If `i` is not :attr:`decomposable <is_decomposable>`, an empty list is returned. Args: i: Any integer. Returns: A decomposition of `i` into powers of two. """ if not cls.is_decomposable(i): return [] powers = [] x = 1 while x <= i: if x & i: powers.append(x) x <<= 1 return powers
[docs] @classmethod def is_decomposable(cls, i: int, /) -> bool: """Check if `i` is decomposable into bitmask values. An integer `i` is decomposable if and only if `i > 2`, and `i` is not a power of two. Args: i: Any integer. Returns: ``True`` if `i` is decomposable into powers of two. """ return i > 2 and ((i & (-i)) != i) # noqa: PLR2004
@staticmethod def _from_toml_records(records: list[TomlOverrideRecord]) -> dict[IdType, str]: permitted = {"id", "override"} overrides = {} for i, record in enumerate(records, start=1): if permitted != set(record): msg = f"Record {i}/{len(records)} is malformed: Expected keys {permitted} but got {record}" raise ValueError(msg) key = record["id"] if key in overrides: raise ValueError(f"Duplicate ID in record {i}/{len(records)}: {record=}") overrides[key] = record["override"] return overrides