statcatlib 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ """Sentinel types for selective relationship loading.
2
+
3
+ When a QueryBuilder uses .include() to request only specific relationships,
4
+ unloaded fields are set to an UNLOADED sentinel rather than None or [].
5
+ Accessing any attribute or iterating over an unloaded field raises
6
+ NotLoadedError with a message that points to the fix.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Never
12
+
13
+ # One cached sentinel instance per field name so identity checks work
14
+ # and we don't create unnecessary objects.
15
+ _cache: dict[str, _UnloadedType] = {}
16
+
17
+
18
+ class NotLoadedError(AttributeError):
19
+ """Raised when accessing a relationship that was not loaded in this query.
20
+
21
+ Example::
22
+
23
+ ds = store.query(BaseDataset).include("publisher").first()
24
+ ds.themes # NotLoadedError: 'themes' was not loaded.
25
+ # Add 'themes' to .include() or call .full().
26
+ """
27
+
28
+ def __init__(self, field: str) -> None:
29
+ super().__init__(f"'{field}' was not loaded. Add '{field}' to .include() or call .full() to load everything.")
30
+ self.field = field
31
+
32
+
33
+ class _UnloadedType:
34
+ """Per-field sentinel placed on relationship fields that were not loaded.
35
+
36
+ Typed as ``Any`` at the usage site so it does not pollute model type
37
+ annotations — the contract is runtime, not static (same approach as
38
+ SQLAlchemy lazy-loading proxies).
39
+
40
+ Do not instantiate directly; use ``make_unloaded(field)`` instead.
41
+ """
42
+
43
+ __slots__ = ("_field",)
44
+
45
+ def __init__(self, field: str) -> None:
46
+ object.__setattr__(self, "_field", field)
47
+
48
+ # Attribute access — covers ds.publisher.name, ds.themes[0].label, etc.
49
+ def __getattr__(self, name: str) -> Never:
50
+ raise NotLoadedError(object.__getattribute__(self, "_field"))
51
+
52
+ # Iteration — covers `for t in ds.themes`
53
+ def __iter__(self) -> Never:
54
+ raise NotLoadedError(object.__getattribute__(self, "_field"))
55
+
56
+ # len() — covers `len(ds.themes)`
57
+ def __len__(self) -> Never:
58
+ raise NotLoadedError(object.__getattribute__(self, "_field"))
59
+
60
+ # Indexing — covers ds.themes[0]
61
+ def __getitem__(self, key: object) -> Never:
62
+ raise NotLoadedError(object.__getattribute__(self, "_field"))
63
+
64
+ # bool() — return False so `if ds.publisher:` doesn't raise;
65
+ # the error fires on attribute access, iteration, len(), or indexing instead.
66
+ def __bool__(self) -> bool:
67
+ return False
68
+
69
+ def __repr__(self) -> str:
70
+ field = object.__getattribute__(self, "_field")
71
+ return f"UNLOADED({field!r})"
72
+
73
+
74
+ def make_unloaded(field: str) -> Any:
75
+ """Return a cached UNLOADED sentinel for *field*.
76
+
77
+ One of the few places `Any` is the honest annotation rather than a hole.
78
+ The sentinel is assigned into model fields of every declared type — a
79
+ `Agent`, a `list[Distribution]`, an `InternationalString` — so the return
80
+ genuinely needs the erased type back out, which is the litmus for `Any`
81
+ over `object`. `object` here would push a narrowing onto every assignment
82
+ it was designed to satisfy.
83
+
84
+ The hole is one line wide: the value is opaque, and touching it raises
85
+ `NotLoadedError` at runtime.
86
+
87
+ (No `# noqa: ANN401` yet — ANN401 is still ignored project-wide until
88
+ #109 step 3, and a suppression that suppresses nothing is the thing
89
+ `reportUnnecessaryTypeIgnoreComment` exists to catch. Add it with the
90
+ rule.)
91
+ """
92
+ if field not in _cache:
93
+ _cache[field] = _UnloadedType(field)
94
+ return _cache[field]
95
+
96
+
97
+ def is_unloaded(value: object) -> bool:
98
+ """Return True if *value* is an UNLOADED sentinel."""
99
+ return isinstance(value, _UnloadedType)