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

1"""Hierarchical scope identifiers for registered ObjectStates.""" 

2 

3from __future__ import annotations 

4 

5from functools import lru_cache 

6from typing import ClassVar 

7 

8 

9class ScopeKey(str): 

10 """A scope id: segments joined by ``SEPARATOR``; the empty key is the global scope. 

11 

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 """ 

16 

17 __slots__ = () 

18 

19 SEPARATOR: ClassVar[str] = "::" 

20 

21 def __new__(cls, value: str | None = "") -> ScopeKey: 

22 return super().__new__(cls, "" if value is None else value) 

23 

24 @classmethod 

25 def of(cls, *parts: str) -> ScopeKey: 

26 """Join ``parts`` into one key; each part is a single segment.""" 

27 

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)) 

32 

33 @property 

34 def is_global(self) -> bool: 

35 return not self 

36 

37 @property 

38 def parts(self) -> tuple[str, ...]: 

39 return tuple(self.split(self.SEPARATOR)) if self else () 

40 

41 @property 

42 def depth(self) -> int: 

43 """Number of segments; the global scope has depth 0.""" 

44 

45 return len(self.parts) 

46 

47 @property 

48 def last(self) -> str: 

49 """The final segment; the empty string for the global scope.""" 

50 

51 return self.parts[-1] if self else "" 

52 

53 @property 

54 def root(self) -> ScopeKey: 

55 """The first segment as a key; the global scope is its own root.""" 

56 

57 return ScopeKey(self.parts[0]) if self else self 

58 

59 @property 

60 def parent(self) -> ScopeKey: 

61 """The key without its final segment; a root's parent is the global scope.""" 

62 

63 return ScopeKey(self.SEPARATOR.join(self.parts[:-1])) 

64 

65 def child(self, segment: str) -> ScopeKey: 

66 """The key one segment below this one.""" 

67 

68 return ScopeKey.of(*self.parts, segment) 

69 

70 def ancestors(self) -> tuple[ScopeKey, ...]: 

71 """The global scope, then each prefix of this key, ending with the key itself.""" 

72 

73 return _ancestors(self) 

74 

75 def contains(self, other: str | None) -> bool: 

76 """Whether ``other`` is this scope or below it.""" 

77 

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) 

82 

83 def strictly_contains(self, other: str | None) -> bool: 

84 """Whether ``other`` is below this scope and not the scope itself.""" 

85 

86 return ScopeKey(other) != self and self.contains(other) 

87 

88 

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.""" 

92 

93 parts = scope.parts 

94 return (ScopeKey(),) + tuple( 

95 ScopeKey(scope.SEPARATOR.join(parts[: index + 1])) for index in range(len(parts)) 

96 )