Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion aggregation/root_vote.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,20 @@ class RootVerdict:
def _basis_of(claim: RootedClaim) -> IndependenceBasis:
"""A claim that does not say how its independence was established has not
established it. Absent, unrecognised and explicitly-unknown all read as
UNKNOWN, which is the conservative direction."""
UNKNOWN, which is the conservative direction.

A member of this enum is accepted as itself. `IndependenceBasis` mixes in
`str`, but `str()` on a member of a `(str, Enum)` returns
"IndependenceBasis.ATTESTED" rather than "attested", so coercing first would
reject the vocabulary's own values. Callers annotate this field as `str`, so
the string path remains the normal one -- but a caller who reaches for the
enum should not have their basis silently downgraded to UNKNOWN.
"""
raw = getattr(claim, "independence_basis", None)
if raw is None:
return IndependenceBasis.UNKNOWN
if isinstance(raw, IndependenceBasis):
return raw
try:
return IndependenceBasis(str(raw))
except ValueError:
Expand Down
43 changes: 43 additions & 0 deletions tests/test_independence_basis.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,49 @@ def test_abstention_is_unaffected_by_basis(self):
self.assertIs(result.verdict, Verdict.ABSTAIN)
self.assertEqual(result.margin, 0)

def test_str_of_a_member_is_not_its_value(self):
"""The reason the enum path needs handling at all.

`IndependenceBasis` mixes in `str`, but `Enum.__str__` wins, so `str()`
on a member yields the qualified name. Coercing with `str()` before
lookup therefore rejects the vocabulary's own members. Pinned here so
the branch in `_basis_of` is not later removed as redundant.
"""
self.assertEqual("IndependenceBasis.ATTESTED", str(IndependenceBasis.ATTESTED))
self.assertNotEqual("attested", str(IndependenceBasis.ATTESTED))

def test_a_member_is_accepted_as_itself(self):
"""A caller reaching for the enum must not be silently downgraded."""
for member in IndependenceBasis:
with self.subTest(basis=member.value):
by_member = verdict([Claim(True, "a", member), Claim(False, "b", member)])
by_string = verdict([Claim(True, "a", member.value),
Claim(False, "b", member.value)])
self.assertEqual(member.value, by_member.weakest_basis)
self.assertEqual(by_string.weakest_basis, by_member.weakest_basis)
self.assertEqual(by_string.basis_counts, by_member.basis_counts)

def test_an_attested_member_still_counts_as_attested(self):
"""The consequence that made this worth fixing.

`attested_margin` and the ATTESTED path in `asymmetric_verdict` both key
off this value. A basis downgraded to UNKNOWN does not merely lose
detail -- it removes a root from the attested count, which is an input
to a decision rather than a label on one.
"""
result = verdict([Claim(True, "a", IndependenceBasis.ATTESTED),
Claim(False, "b", IndependenceBasis.ATTESTED)])
self.assertEqual({"attested": 2}, result.basis_counts)
self.assertEqual(0, result.attested_margin)
self.assertEqual("attested", result.weakest_basis)

def test_unrecognised_values_still_read_as_unknown(self):
"""The conservative direction is preserved for genuine junk."""
for junk in ("ATTESTED", "somewhat-attested", "", 7):
with self.subTest(junk=junk):
result = verdict([Claim(True, "a", junk), Claim(False, "b", "attested")])
self.assertEqual("unknown", result.weakest_basis)


if __name__ == "__main__":
unittest.main()
Loading