Inspecting a codebase¶
A schema is an ordinary annotation, so valgebra reads the annotations a codebase already has — and answers questions about them the interpreter cannot. Nothing is added to the code under study: the schemas live in the script asking the question, and valgebra stays a development dependency.
This page is the recipes. Each is one question, asked of one or two annotations, answered by set reasoning rather than by reading. They suit an agent working on a codebase it did not write, and they need no adoption from that codebase.
For using valgebra as a contract in your own code, see the tutorial and the algebra guide.
The shape of every question¶
Three relations do the work, and each answers a different kind of question:
| Relation | Asks |
|---|---|
is_valid |
is this value in that set? |
is_subtype_of |
is every value of this set in that one? |
is_empty |
does this set contain anything at all? |
The recipes below are compositions of those three over sets the codebase already describes.
A contract the code implies and nothing enforces¶
The strongest question, because it finds bugs rather than untidiness.
A parameter used as a divisor must not be zero. One whose attribute is read must
not be None. One passed to len must be sized. The body states these by
using the value that way — and if the declared type admits a violating value with
no guard on the path, the function is broken for that value.
That is one set difference: declared ∧ ¬implied is exactly the set of values
that pass the type check and break the code.
from valgebra import Validator, complement, intersection, union
def unenforced(declared: object, implied: object) -> list[object]:
"""Values the declaration admits and the body cannot survive."""
breaking = intersection(Validator(declared), complement(Validator(implied)))
probes = [None, 0, 0.0, "", b"", [], {}, False, -1]
return [p for p in probes if breaking.is_valid(p)]
# `def make_grid(columns: int)` whose body computes `idx // columns`.
# The body implies "not zero"; the annotation admits zero.
assert unenforced(int, complement(Validator(0))) == [0]
# `def f(cfg: object)` whose body reads `cfg.model`. Any attribute read implies
# "not None", and `object` admits None.
assert unenforced(object, complement(Validator(None))) == [None]
# A declaration that already excludes the breaking values has nothing to report.
assert unenforced(str, complement(Validator(None))) == []
An unannotated parameter is the same question with the top on the left, which is why the absence of a contract is the loudest answer rather than a silent one:
from valgebra import anything, complement, intersection, Validator
nothing_declared = intersection(anything, complement(Validator(None)))
assert not nothing_declared.is_valid(None) # the body needs non-None
assert anything.is_valid(None) # and nothing stops None arriving
A precondition the code writes down — if n < 1: raise ValueError — is a
refinement schema, and valgebra holds it as one. Any other function using that
value the same way without the check admits exactly what the first one rejects:
from typing import Annotated
import annotated_types as at
from valgebra import Validator, complement, intersection
stated = Validator(Annotated[int, at.Ge(1)]) # what `if n < 1: raise` leaves
unchecked = Validator(int) # what a sibling function accepts
gap = intersection(unchecked, complement(stated))
assert gap.is_valid(0)
assert not gap.is_valid(5)
A branch the annotation makes unreachable¶
An isinstance test asks for a value in both sets at once. When the intersection
is empty the branch is dead — and the interesting case is not that the branch is
wasted, but that the code and its annotation disagree about what arrives.
from valgebra import Validator, intersection
# `def __setitem__(self, key: str, ...)` whose body does `if isinstance(key, bytes)`.
assert intersection(Validator(str), Validator(bytes)).is_empty()
One of the two is wrong, and neither can be read alone. The dual question is a test that can never fail:
from valgebra import Validator
# `def f(sequence: bytes)` guarded by `if not isinstance(sequence, bytes): raise`.
assert Validator(bytes).is_subtype_of(bytes) # the guard can never fire
A union arm another arm already covers¶
bool is a subclass of int, so bool | int is int. Only inclusion between
the arms says so; a type checker accepts the annotation as written.
from valgebra import Validator, union
assert Validator(bool).is_subtype_of(int) # the `bool` arm adds nothing
assert union(bool, int).is_equivalent(int)
assert union(object, None).is_equivalent(object)
An exception a sibling already catches¶
except (OSError, TimeoutError) names two classes where one contains the other,
because TimeoutError became an OSError subclass in Python 3.3. Inside one
tuple that is redundancy; across two clauses it is a handler that never runs.
from valgebra import Validator
assert Validator(TimeoutError).is_subtype_of(OSError)
assert Validator(ModuleNotFoundError).is_subtype_of(ImportError)
An annotation that admits nothing¶
An unsatisfiable annotation accepts no value at all, and no test that feeds it valid input will ever say so.
from typing import Annotated
import annotated_types as at
from valgebra import Validator, complement, intersection
assert Validator(Annotated[int, at.Gt(10), at.Lt(5)]).is_empty()
assert intersection(int, complement(int)).is_empty()
assert intersection(int, str).is_empty() # disjoint kinds
An override that narrows what its base accepts¶
Liskov, decided rather than reviewed: an override must accept everything the base accepts, so its parameter set is a superset.
from valgebra import Validator
base_accepts = Validator(str | None)
override_accepts = str
assert not base_accepts.is_subtype_of(override_accepts) # the override narrows
What a workload actually passed¶
Static reading sees literals. A workload sees values, and the same three relations answer three more questions of them: whether a value arrived that the declaration rejects, whether the declaration is wider than anything observed, and which arms of a union the workload never reached.
The last has no analogue in a coverage report: a line report says the branch ran,
and cannot say which members of int | str ever arrived.
from valgebra import Validator, union
declared = Validator(int | str)
observed = [1, 2, 3] # collected by a tracer over a real run
assert all(declared.is_valid(value) for value in observed) # no value escapes
seen = union(*[Validator(type(value)) for value in observed])
assert seen.is_subtype_of(int | str)
assert not declared.is_subtype_of(seen) # wider than anything observed
assert not any(Validator(str).is_valid(value) for value in observed) # `str` untested
Reading a negative answer¶
is_valid is exact: False means the value is not a member.
is_subtype_of, is_equivalent and is_empty are sound, not complete. A
True is a proof; a False means not proven, which is not the same as
disproven. A recipe that reads not a.is_subtype_of(b) as "a narrowing happened"
reports a change that may not have occurred.
The decidability boundary states where the answers are exact. The place that bites an inspection script is a deeply nested Boolean combination, where the work is a product of the branches. A table of literals is not one of them: it denotes a finite set and is decided by membership, at any width and in both directions.
What this cannot see¶
- A codebase that declares nothing is invisible to the static recipes; only the workload recipes reach it.
- Names must resolve. An annotation naming a project type cannot be compiled without importing the module, and importing runs it. Resolving with the module's own namespace reaches every annotation and executes the code; reading the source reaches only what the standard library names. Pick deliberately.
- A declaration is not a missing return. A
Protocolmethod, an@overloadand a docstring-only stub all reach the end of their body without returning. intwherefloatis declared is conformant. The typing spec grants an implicit promotion; valgebra decidesisinstance, where the two are disjoint. An inspection script must classify that apart rather than report it.- A finding needs a witness. These recipes answer questions about sets; a concrete value from the offending set is what makes an answer checkable by a human, and one that cannot produce a value is not yet a finding.
- A compiled schema does not enumerate itself. There is no call returning a
validator's declared keys, its per-field clauses, or its arms. Every question
above is asked by comparing a schema to another schema — that is the whole
interface, and it is enough for the questions on this page.
reprrenders the annotation that produces a schema and is meant for a human to read; parsing it to recover structure is not supported and will break. To check that a schema and a class declare the same fields, write the field set down once and build both from it, rather than reading it back out of either.