Deep-freeze conversion and the structural immutability check (IPL-3).
Two functions with one relationship: :func:deep_freeze is what the compiler
runs over every value it accepts, and :func:is_deeply_frozen is what CI runs
over the plan it produced. The compiler converts rather than trusting its
inputs to already be immutable, so a caller that hands in a list and then
mutates it cannot change a compiled plan.
The recursion deliberately stops at objects that are neither containers nor
frozen dataclasses. That is IPL-4, not an oversight: plan group G6 holds live
port objects whose identity is frozen and whose internal state is not. A
connection pool is not immutable and pretending otherwise would make the rule
unimplementable.
deep_freeze
deep_freeze(value: Any) -> Any
Return an immutable equivalent of value, transitively.
list/tuple become tuple, set/frozenset become
frozenset, mappings become read-only proxies over a fresh dict, and
everything else is returned unchanged.
Source code in src/symfonic/kernel/contracts/freeze.py
| def deep_freeze(value: Any) -> Any:
"""Return an immutable equivalent of ``value``, transitively.
``list``/``tuple`` become ``tuple``, ``set``/``frozenset`` become
``frozenset``, mappings become read-only proxies over a fresh dict, and
everything else is returned unchanged.
"""
if isinstance(value, MappingProxyType):
return MappingProxyType({key: deep_freeze(item) for key, item in value.items()})
if isinstance(value, Mapping):
return MappingProxyType({key: deep_freeze(item) for key, item in value.items()})
if isinstance(value, list | tuple):
return tuple(deep_freeze(item) for item in value)
if isinstance(value, set | frozenset):
return frozenset(deep_freeze(item) for item in value)
if isinstance(value, bytearray):
return bytes(value)
return value
|
freeze_mapping
freeze_mapping(value: Mapping[str, Any] | None) -> Mapping[str, Any]
Deep-freeze a mapping, treating None as empty.
Source code in src/symfonic/kernel/contracts/freeze.py
| def freeze_mapping(value: Mapping[str, Any] | None) -> Mapping[str, Any]:
"""Deep-freeze a mapping, treating ``None`` as empty."""
if not value:
return MappingProxyType({})
frozen = deep_freeze(dict(value))
return frozen # type: ignore[return-value]
|
is_deeply_frozen
is_deeply_frozen(value: Any) -> bool
Return True when no mutable container is reachable from value.
"Frozen at the top, list underneath" is exactly what this rejects.
Source code in src/symfonic/kernel/contracts/freeze.py
| def is_deeply_frozen(value: Any) -> bool:
"""Return ``True`` when no mutable container is reachable from ``value``.
"Frozen at the top, ``list`` underneath" is exactly what this rejects.
"""
if isinstance(value, MappingProxyType):
return all(is_deeply_frozen(item) for item in value.values())
if isinstance(value, _MUTABLE_CONTAINERS):
return False
if isinstance(value, str | bytes):
return True
if isinstance(value, tuple | frozenset):
return all(is_deeply_frozen(item) for item in value)
if dataclasses.is_dataclass(value) and not isinstance(value, type):
params = getattr(type(value), "__dataclass_params__", None)
if params is not None and not params.frozen:
return False
return all(
is_deeply_frozen(getattr(value, field.name))
for field in dataclasses.fields(value)
)
return True
|