Coverage for src/objectstate/scope_key.py: 100%
47 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 07:22 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 07:22 +0000
1"""Hierarchical scope identifiers for registered ObjectStates."""
3from __future__ import annotations
5from functools import lru_cache
6from typing import ClassVar
9class ScopeKey(str):
10 """A scope id: segments joined by ``SEPARATOR``; the empty key is the global scope.
12 A scope contains itself and every scope whose segments extend its own; the
13 global scope contains every scope. ``ScopeKey`` is a ``str``, so it is a
14 dictionary key and serializes as its text.
15 """
17 __slots__ = ()
19 SEPARATOR: ClassVar[str] = "::"
21 def __new__(cls, value: str | None = "") -> ScopeKey:
22 return super().__new__(cls, "" if value is None else value)
24 @classmethod
25 def of(cls, *parts: str) -> ScopeKey:
26 """Join ``parts`` into one key; each part is a single segment."""
28 for part in parts:
29 if not part or cls.SEPARATOR in part:
30 raise ValueError(f"Scope segment {part!r} is empty or contains {cls.SEPARATOR!r}.")
31 return cls(cls.SEPARATOR.join(parts))
33 @property
34 def is_global(self) -> bool:
35 return not self
37 @property
38 def parts(self) -> tuple[str, ...]:
39 return tuple(self.split(self.SEPARATOR)) if self else ()
41 @property
42 def depth(self) -> int:
43 """Number of segments; the global scope has depth 0."""
45 return len(self.parts)
47 @property
48 def last(self) -> str:
49 """The final segment; the empty string for the global scope."""
51 return self.parts[-1] if self else ""
53 @property
54 def root(self) -> ScopeKey:
55 """The first segment as a key; the global scope is its own root."""
57 return ScopeKey(self.parts[0]) if self else self
59 @property
60 def parent(self) -> ScopeKey:
61 """The key without its final segment; a root's parent is the global scope."""
63 return ScopeKey(self.SEPARATOR.join(self.parts[:-1]))
65 def child(self, segment: str) -> ScopeKey:
66 """The key one segment below this one."""
68 return ScopeKey.of(*self.parts, segment)
70 def ancestors(self) -> tuple[ScopeKey, ...]:
71 """The global scope, then each prefix of this key, ending with the key itself."""
73 return _ancestors(self)
75 def contains(self, other: str | None) -> bool:
76 """Whether ``other`` is this scope or below it."""
78 other_key = ScopeKey(other)
79 if self.is_global or other_key == self:
80 return True
81 return other_key.startswith(self + self.SEPARATOR)
83 def strictly_contains(self, other: str | None) -> bool:
84 """Whether ``other`` is below this scope and not the scope itself."""
86 return ScopeKey(other) != self and self.contains(other)
89@lru_cache(maxsize=4096)
90def _ancestors(scope: ScopeKey) -> tuple[ScopeKey, ...]:
91 """``ScopeKey.ancestors``, kept per key: every resolved read of a state asks for it."""
93 parts = scope.parts
94 return (ScopeKey(),) + tuple(
95 ScopeKey(scope.SEPARATOR.join(parts[: index + 1])) for index in range(len(parts))
96 )