compound_keys#

Hashable, serialisable keys built from two or more atomic string parts, with optional orientation and built-in rename / container-migration helpers.

Two concrete classes are provided:

  • CompoundKey — N ≥ 2 parts, general case.

  • PairKey — exactly 2 parts; preferred API for pairs. CompoundKey.from_string / from_tuple auto-promote to PairKey when the result has exactly 2 parts.

Serialisation#

Both classes serialise to 'part0<sep>part1<sep>…' (default '-'). Unoriented keys are stored in sorted (canonical) order so that PairKey('B', 'A') == PairKey('A', 'B').

Typical use#

>>> from ClearMap.config.compound_keys import PairKey, CompoundKey
>>> k = PairKey('cfos', 'dapi')
>>> str(k)
'cfos-dapi'
>>> k2 = PairKey.from_string('cfos-dapi')
>>> k == k2
True
class CompoundKey(*parts: str, oriented: bool = False, sep: str = '-')[source]#

Bases: object

An immutable, hashable key composed of N ≥ 2 atomic string parts.

For the common two-part case prefer PairKey, which adds .a / .b accessors and pair-specific container helpers. CompoundKey.from_string and from_tuple return a PairKey automatically when the result has exactly two parts.

Orientation#

oriented=False (default)

Parts are sorted on construction so that key identity is independent of argument order.

oriented=True

Part order is preserved; ('A', 'B') != ('B', 'A').

param *parts:

Two or more atomic string parts.

type *parts:

str

param oriented:

Whether part order is significant.

type oriented:

bool

param sep:

Separator used for string serialisation and parsing. Defaults to DEFAULT_PAIR_SEP

type sep:

str

classmethod canonical_str(key: str | tuple[str, ...], oriented: bool) str[source]#

Return the canonical (sorted) string representation of key.

Accepts either a serialised string or a raw tuple of parts.

Parameters:
  • key (str | tuple[str, ...]) – Key to canonicalise.

  • oriented (bool) – Whether to treat the key as oriented before canonicalising.

classmethod from_string(s: str, *, oriented: bool = False, sep: str = '-') CompoundKey | PairKey[source]#

Parse a serialised key string such as 'cfos-dapi'.

Automatically returns a PairKey when called on CompoundKey and the string contains exactly one separator.

Parameters:
  • s (str) – Serialised key, e.g. 'a-b' or 'a-b-c'.

  • oriented (bool) – Whether part order is significant.

  • sep (str) – Expected separator character(s).

Returns

CompoundKey | PairKey

Raises:

ValueError – If the string yields fewer parts than _min_parts or a number inconsistent with _max_parts.

classmethod from_tuple(parts: tuple[str, ...], *, oriented: bool = False, sep: str = '-') CompoundKey | PairKey[source]#

Construct from an existing tuple of parts.

Automatically returns a PairKey when called on CompoundKey and len(parts) == 2.

Parameters:
  • parts (tuple[str, ...]) – Atomic parts.

  • oriented (bool) – Whether part order is significant.

  • sep (str) – Separator for serialisation.

Returns

CompoundKey | PairKey

classmethod is_valid_key_str(s: object, *, sep: str = '-') bool[source]#

Return True if s is a string that can be parsed by this class.

Checks separator count against _min_parts / _max_parts and ensures no part is empty or itself contains the separator.

Parameters:
  • s (object) – Candidate string.

  • sep (str) – Separator to split on.

classmethod migrate_container_payload(container: Mapping[str, Any], *, new_key: str, rename_map: Mapping[str, str], oriented: bool) tuple[Any | None, bool][source]#

Look up a stored value for new_key, falling back to any plausible predecessor key produced by candidate_ancestors_from_renames().

Use this when a channel was renamed and the config entry was written under the old key: the new key will not be found directly, but a predecessor key will be.

Parameters:
  • container (Mapping[str, Any]) – The config mapping to search.

  • new_key (str) – The compound-key string under which a value is desired.

  • rename_map (Mapping[str, str]) – Old → new name mapping used to derive candidate predecessors.

  • oriented (bool) – Whether keys should be treated as oriented.

Returns

tuple[Optional[Any], bool]

(value, True) if a predecessor key was found; (None, False) otherwise.

classmethod normalize_container_keys(cur: Mapping[str, Any], *, oriented: bool) tuple[dict[str, Any], bool][source]#

Re-serialise every valid compound-key string in cur to its canonical form (sorted for unoriented keys).

Parameters:
  • cur (Mapping[str, Any]) – Source mapping whose keys may include compound-key strings.

  • oriented (bool) – Whether keys should be treated as oriented.

Returns

tuple[dict[str, Any], bool]

(normalised_dict, changed).

classmethod prune_container_invalid_atoms(container: Mapping[str, Any], *, allowed_atoms: set[str], oriented: bool) dict[str, Any][source]#

Remove entries whose compound-key string contains a part not in allowed_atoms.

Parameters:
  • container (Mapping[str, Any]) – Source mapping to filter.

  • allowed_atoms (set[str]) – The set of permitted atomic part values.

  • oriented (bool) – Whether keys should be treated as oriented.

Returns

dict[str, Any]

Filtered copy of container.

classmethod rename_container_keys(cur: Mapping[str, Any], rename_map: Mapping[str, str], *, oriented: bool, max_hops: int = 32) tuple[dict[str, Any], bool][source]#

Apply rename_map to the parts of every compound-key string in cur.

Parameters:
  • cur (Mapping[str, Any]) – Source mapping.

  • rename_map (Mapping[str, str]) – Old → new name mapping.

  • oriented (bool) – Whether keys should be treated as oriented.

  • max_hops (int) – Maximum transitive rename steps per part.

Returns

tuple[dict[str, Any], bool]

(renamed_dict, changed).

classmethod rename_key(key: str, rename_map: Mapping[str, str], *, oriented: bool, max_hops: int = 32) str[source]#

Parse key, apply rename_map to its parts, and return the serialised result.

Parameters:
  • key (str) – Serialised key to rename.

  • rename_map (Mapping[str, str]) – Old → new name mapping.

  • oriented (bool) – Whether the key should be treated as oriented.

  • max_hops (int) – Maximum transitive rename steps per part.

classmethod split_str(key: str, oriented: bool) tuple[str, ...][source]#

Parse key and return its parts as a tuple.

Convenience wrapper around from_string() + as_tuple().

Parameters:
  • key (str) – Serialised key string.

  • oriented (bool) – Whether to treat the key as oriented.

as_tuple() tuple[str, ...][source]#

Return parts as a plain tuple (same object as stored internally).

candidate_ancestors_from_renames(rename_map: Mapping[str, str]) list[CompoundKey][source]#

Return every key that this key could have been before a rename.

Background#

Keys are stored in config under their name at the time of writing. When channels are renamed, the stored key becomes stale. This method generates all plausible old keys so callers can look them up and carry the stored value forward.

Algorithm#

Given rename_map {“old”: “new”, …}, invert it to find what each current part used to be called. Then take every combination of (current-or-predecessor) across all positions, discarding combos that equal self (nothing was renamed) or contain the same value twice (illegal key).

For oriented keys position order is preserved. For unoriented keys the constructor sorts parts into canonical form.

param rename_map:

Maps old name → new name. Only direct (single-hop) renames are considered.

type rename_map:

Mapping[str, str]

Examples

PairKey, oriented — two renames, one per side:

>>> PairKey('C', 'D', oriented=True).candidate_ancestors_from_renames({'A': 'C', 'B': 'D'})
[PairKey(a='A', b='B', oriented=True, sep='-'),
 PairKey(a='A', b='D', oriented=True, sep='-'),
 PairKey(a='C', b='B', oriented=True, sep='-')]

Position 0 was C, or A (which was renamed to C). Position 1 was D, or B (which was renamed to D). All combinations except self (‘C’,’D’) are returned.

PairKey, unoriented — same rename map:

>>> PairKey('C', 'D', oriented=False).candidate_ancestors_from_renames({'A': 'C', 'B': 'D'})
[PairKey(a='A', b='B', oriented=False, sep='-'),
 PairKey(a='A', b='D', oriented=False, sep='-'),
 PairKey(a='B', b='C', oriented=False, sep='-')]

Same combinations, but each pair is sorted by the constructor: (‘C’,’B’) becomes ‘B-C’.

PairKey — rename does not touch either part:

>>> PairKey('C', 'D', oriented=True).candidate_ancestors_from_renames({'X': 'Y'})
[]

CompoundKey triplet, oriented:

>>> CompoundKey('C', 'D', 'E', oriented=True).candidate_ancestors_from_renames(
...     {'A': 'C', 'B': 'E'})
[CompoundKey("A", "D", "B", oriented=True, sep="-"),
 CompoundKey("A", "D", "E", oriented=True, sep="-"),
 CompoundKey("C", "D", "B", oriented=True, sep="-")]

Position 0 options: {C, A}. Position 1 options: {D} (no rename). Position 2 options: {E, B}. Self (‘C’,’D’,’E’) is discarded.

CompoundKey triplet, unoriented:

>>> CompoundKey('C', 'D', 'E', oriented=False).candidate_ancestors_from_renames(
...     {'A': 'C', 'B': 'E'})
[CompoundKey("A", "D", "B", oriented=False, sep="-"),
 CompoundKey("A", "D", "E", oriented=False, sep="-"),
 CompoundKey("B", "C", "D", oriented=False, sep="-")]

Same combinations; each triplet is sorted by the constructor: (‘C’,’D’,’B’) becomes (‘B’,’C’,’D’).

canonical() CompoundKey[source]#

Return the unoriented (sorted) form of this key.

If the key is already unoriented, returns self unchanged. The returned object has the same concrete type (PairKey subclass is preserved via type(self)).

rename(rename_map: Mapping[str, str], *, max_hops: int = 32) CompoundKey | PairKey[source]#

Return a new key with each part renamed according to rename_map.

Renames are applied transitively (up to max_hops steps). Unoriented keys are re-canonicalized as usual by the constructor after renaming.

Parameters:
  • rename_map (Mapping[str, str]) – Old → new name mapping.

  • max_hops (int) – Maximum transitive rename steps per part.

swapped() CompoundKey[source]#

Return a new key with parts in reversed order.

For a PairKey this swaps a and b. For longer keys parts are fully reversed. The oriented flag is preserved.

validate_atoms(allowed: Iterable[str]) bool#

Return True if every part of this key is in allowed.

Parameters:

allowed (Iterable[str]) – The set of permitted atomic values.

validate_parts(allowed: Iterable[str]) bool[source]#

Return True if every part of this key is in allowed.

Parameters:

allowed (Iterable[str]) – The set of permitted atomic values.

SEP: str = '-'#
property n: int#

Number of parts.

property oriented: bool#

Whether part order is significant for equality and hashing.

property parts: tuple[str, ...]#

All parts as an immutable tuple.

property sep: str#

Separator used for string serialisation.

class PairKey(a: str, b: str, *, oriented: bool = False, sep: str = '-')[source]#

Bases: CompoundKey

CompoundKey specialised for exactly two atomic parts.

CompoundKey class-methods (from_string, from_tuple) automatically return a PairKey when the result has two parts, so callers rarely need to instantiate this class directly.

All rename / container helpers defined on CompoundKey work unchanged; PairKey only adds .a / .b property accessors and narrows return-type annotations.

Parameters:
  • a (str) – The two atomic parts.

  • b (str) – The two atomic parts.

  • oriented (bool) – If True, (a, b) != (b, a). If False (default), parts are sorted so order does not matter.

  • sep (str) – Separator used for string serialisation.

as_tuple() tuple[str, str][source]#

Return (a, b) as a two-element tuple.

canonical() PairKey[source]#

Return the unoriented (sorted) form; type is preserved as PairKey.

swapped() PairKey[source]#

Return a new PairKey with a and b exchanged.

SEP: str = '-'#
property a: str#

First part (lower of the two for unoriented keys).

property b: str#

Second part (higher of the two for unoriented keys).