# SPDX-License-Identifier: LGPL-3.0-or-later
"""
Asking about a type, rather than about a value: what it is, whether it can be
validated against, and — when it cannot — precisely what stopped it.
The structure is a real artifact, built from the node model, which exists anyway
to serve the validators.
"""
# In v1 this was a side effect of a validation walk: an inspector was passed *as
# the value* into validate, and every branch carried an arm that recorded itself
# instead of checking. One walk served two purposes, so every new type form had
# to be implemented three times in lockstep, and forgetting one produced a silent
# gap rather than an error. See DESIGN.md §3.5.
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any
from . import _cache as cache
from .nodes import TypeNode, node_for
__all__ = (
"can_validate",
"clear_cache",
"forget_type",
"inspect_type",
"scoped_cache",
)
[docs]
def inspect_type(t: Any, /) -> TypeNode:
"""
The structure of a type: its shape, its components, and whether each is
supported.
When a type is unsupported this reports the **whole** structure and marks
precisely which component poisoned it. Totality means the answer to *"can
this be validated"* is then always "no", but it should never be an opaque
"no" — use :meth:`~typing_validation.nodes.TypeNode.unsupported_components`
to name the culprits.
"""
return node_for(t)
[docs]
def can_validate(t: Any, /) -> bool:
"""
Whether this library can validate against a type at all.
Support is all-or-nothing: ``tuple[int, Callable[[int], int]]`` answers
:obj:`False` even though the ``int`` component is perfectly checkable,
because a validation that silently skipped part of its obligation would be
worse than none. This function exists so a caller can ask up front.
Note that :func:`~typing_validation.validation.validate` is *lazier* than this. It walks
the value and the type together and raises only when it reaches an
unsupported component, so ``validate([], list[Callable[[int], int]])``
returns :obj:`True` while this returns :obj:`False`. That is deliberate:
scanning the type on every call is exactly the overhead that mechanism exists
to avoid. This is the total answer, and it is the one to branch on.
"""
return node_for(t).supported
[docs]
def clear_cache() -> None:
"""
Drop every interned node.
Safe by construction: interning is never semantically observable, so this
changes cost and never an answer. Without that guarantee, clearing a cache
would be a semantic operation and no user could be expected to reason about
it.
"""
cache.clear()
[docs]
def forget_type(t: Any, /) -> bool:
"""
Drop the interned node for one type, if it has one.
Returns whether anything was dropped. Note that nodes for its *components*
are untouched and may still be shared by other types.
"""
return cache.forget(t)
[docs]
@contextmanager
def scoped_cache() -> Iterator[None]:
"""
Intern nodes into a temporary tier, dropped whole on exit.
For callers who want the sharing without the retention: a strong reference to
a type transitively pins the classes it mentions, and through them their
modules and closures, so a process that builds types dynamically — synthesised
``TypedDict``\\ s, classes from a factory, types built per request — would
otherwise accumulate them forever.
While the tier is active, lookups consult tiers innermost-first and every new
node is created in the innermost tier. Exiting drops that tier in one
operation, with no per-entry bookkeeping::
with scoped_cache():
for spec in incoming_specs:
t = build_a_type_from(spec)
if can_validate(t):
report(inspect_type(t))
Note that :func:`~typing_validation.validation.validate` is **not** what this
is for: it analyses nothing and interns nothing, so it neither fills this tier
nor benefits from it. What fills the cache is asking *about* a type —
:func:`can_validate`, :func:`inspect_type`, and the explanation built when a
validation fails.
Nodes created inside may reference nodes in enclosing tiers, which outlive
them; nothing enclosing can reference inward, because while this tier is
active it is where all new nodes go. So dropping it can never leave a
dangling reference — and can never change an answer, only a cost.
"""
# TODO: revisit this example when the reusable validators land. They are the
# mechanism that makes scoping genuinely compelling — one cached closure per
# type, built from a type the caller synthesised — whereas today the only
# clients are the introspection functions, which makes the case for scoping
# real but thin.
cache.push_tier()
try:
yield
finally:
cache.pop_tier()