From 865daf81a821c0b1d1bd1dd140c54519e18e082d Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Thu, 23 Dec 2021 12:26:25 +0100 Subject: [PATCH 1/6] types: allow only List[List[Candidate]] and Tuple[Tuple[Candidate]] instead of Sequence[Sequence[Candidate]] This makes type checking easier, and prevents false-positive inputs (string, bytearray etc are also Sequences). --- schulze_condorcet/types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/schulze_condorcet/types.py b/schulze_condorcet/types.py index e3cc6a3..fcf4800 100644 --- a/schulze_condorcet/types.py +++ b/schulze_condorcet/types.py @@ -1,5 +1,5 @@ from typing import ( - Dict, List, NewType, Protocol, Tuple, TypedDict, Sequence + Dict, List, NewType, Protocol, Tuple, TypedDict ) @@ -11,7 +11,9 @@ Candidate = NewType('Candidate', str) # A single vote, split into separate levels accordingly to (descending) preference. # All candidates at the same level (in the same inner tuple) have equal preference. -VoteList = Sequence[Sequence[Candidate]] +VoteTuple = Tuple[Tuple[Candidate, ...], ...] +# We accept VoteLists instead of VoteTuples for convenience. +VoteList = List[List[Candidate]] # How many voters prefer the first candidate over the second candidate. PairwisePreference = Dict[Tuple[Candidate, Candidate], int] # The link strength between two candidates. From 68e55764289578707ce4a8494e3fee40c61586cf Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Thu, 23 Dec 2021 12:34:05 +0100 Subject: [PATCH 2/6] io: Add io-interface This provides some convenient conversion functions to construct the string-based view on votes from the level-based one and vice versa. closes #25 --- schulze_condorcet/io.py | 66 ++++++++++++++++++++++++++ schulze_condorcet/schulze_condorcet.py | 30 ++---------- 2 files changed, 71 insertions(+), 25 deletions(-) create mode 100644 schulze_condorcet/io.py diff --git a/schulze_condorcet/io.py b/schulze_condorcet/io.py new file mode 100644 index 0000000..707129d --- /dev/null +++ b/schulze_condorcet/io.py @@ -0,0 +1,66 @@ +"""Offer convenient functions to convert votes and candidates into proper types. + +This especially includes marking strings as Candidate or VoteString, and converting the +string-based representations of votes (like 'a>b=c=d>e') into the level-based +representations of votes (like [[a], [b, c, d], [e]]) and vice versa. +""" + +from typing import Collection, List, Sequence, Union + +from schulze_condorcet.types import Candidate, VoteList, VoteString, VoteTuple + + +def as_candidate(value: str) -> Candidate: + """Mark a string as candidate.""" + return Candidate(value) + + +def as_candidates(values: Sequence[str]) -> List[Candidate]: + """Mark a row of strings as candidates. + + We respect the order of the candidates, as this is also respected during evaluation + of the votes using the schulze method. + """ + return [Candidate(value) for value in values] + + +def as_vote_string( + value: Union[str, VoteList, VoteTuple] +) -> VoteString: + """Convert a level-based representation of the vote into its string representation. + + This also accepts a string which will be marked as vote for convenience. + """ + if isinstance(value, str): + return VoteString(value) + elif isinstance(value, (list, tuple)): + return VoteString(">".join("=".join(level) for level in value)) + else: + raise NotImplementedError(value) + + +def as_vote_strings( + values: Union[Collection[str], Collection[VoteList], Collection[VoteTuple]] +) -> List[VoteString]: + """Convert each value into a string representation of the vote.""" + return [as_vote_string(value) for value in values] + + +def as_vote_tuple( + value: Union[str, VoteString] +) -> VoteTuple: + """Convert a string representation of a vote into its level-based representation.""" + return ( + tuple( + tuple( + Candidate(candidate) for candidate in level.split('=') + ) for level in value.split('>') + ) + ) + + +def as_vote_tuples( + values: Union[Collection[str], Collection[VoteString]] +) -> List[VoteTuple]: + """Convert string representations of votes into their level-based representations.""" + return [as_vote_tuple(value) for value in values] diff --git a/schulze_condorcet/schulze_condorcet.py b/schulze_condorcet/schulze_condorcet.py index 53ba864..404337c 100644 --- a/schulze_condorcet/schulze_condorcet.py +++ b/schulze_condorcet/schulze_condorcet.py @@ -4,10 +4,11 @@ Collection, Container, List, Mapping, Tuple, Sequence ) +from schulze_condorcet.io import as_vote_string, as_vote_tuples from schulze_condorcet.strength import winning_votes from schulze_condorcet.types import ( Candidate, DetailedResultLevel, LinkStrength, PairwisePreference, SchulzeResult, - VoteList, StrengthCallback, VoteString + StrengthCallback, VoteString ) @@ -43,27 +44,6 @@ def _schulze_winners(d: Mapping[Tuple[Candidate, Candidate], int], return winners -def _split_vote(vote: VoteString) -> VoteList: - """Split a vote string into its candidates.""" - return ( - tuple( - tuple( - Candidate(candidate) for candidate in level.split('=') - ) for level in vote.split('>') - ) - ) - - -def _split_votes(votes: Collection[VoteString]) -> List[VoteList]: - """Split a list of vote strings into their candidates.""" - return [_split_vote(vote) for vote in votes] - - -def _recombine_vote(vote: VoteList) -> VoteString: - """Construct a vote string from its candidates, opposite of _split_vote.""" - return VoteString(">".join("=".join(level) for level in vote)) - - def _check_consistency(votes: Collection[VoteString], candidates: Sequence[Candidate]) -> None: """Check that the given vote strings are consistent with the provided candidates. @@ -73,7 +53,7 @@ def _check_consistency(votes: Collection[VoteString], candidates: Sequence[Candi if any(">" in candidate or "=" in candidate for candidate in candidates): raise ValueError(_("A candidate contains a forbidden character.")) candidates_set = set(candidates) - for vote in _split_votes(votes): + for vote in as_vote_tuples(votes): vote_candidates = [c for c in itertools.chain.from_iterable(vote)] vote_candidates_set = set(vote_candidates) if candidates_set != vote_candidates_set: @@ -102,7 +82,7 @@ def _pairwise_preference( ) -> PairwisePreference: """Calculate the pairwise preference of all candidates from all given votes.""" counts = {(x, y): 0 for x in candidates for y in candidates} - for vote in _split_votes(votes): + for vote in as_vote_tuples(votes): for x in candidates: for y in candidates: if _subindex(vote, x) < _subindex(vote, y): @@ -183,7 +163,7 @@ def schulze_evaluate( _, result = _schulze_evaluate_routine(votes, candidates, strength) # Construct a vote string reflecting the overall preference - return _recombine_vote(result) + return as_vote_string(result) def schulze_evaluate_detailed( From bdff631892abb40a02d0a33107da35bb9340925d Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Mon, 27 Dec 2021 14:56:46 +0100 Subject: [PATCH 3/6] tests: add io tests --- tests/test_schulze.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_schulze.py b/tests/test_schulze.py index 0a82622..449939e 100644 --- a/tests/test_schulze.py +++ b/tests/test_schulze.py @@ -4,6 +4,7 @@ import unittest from schulze_condorcet import schulze_evaluate, schulze_evaluate_detailed +import schulze_condorcet.io as io from schulze_condorcet.strength import margin, winning_votes from schulze_condorcet.types import Candidate, DetailedResultLevel as DRL, VoteString @@ -517,6 +518,36 @@ def test_result_order(self) -> None: condensed = schulze_evaluate(reference_votes, candidates) self.assertEqual("1=0>2", condensed) + def test_io(self) -> None: + candidates = ["1", "2", "3"] + # This does only static type conversion + self.assertEqual(io.as_candidates(candidates), candidates) + + # build the same votes, represented as string, list and tuple + vote_str_1 = "1" + vote_str_2 = "1=2>3" + vote_list_1 = [[Candidate("1")]] + vote_list_2 = [[Candidate("1"), Candidate("2")], [Candidate("3")]] + vote_tuple_1 = ((Candidate("1"),),) + vote_tuple_2 = ((Candidate("1"), Candidate("2")), (Candidate("3"),)) + vote_str_list = [vote_str_1, vote_str_2] + vote_list_list = [vote_list_1, vote_list_2] + vote_tuple_list = [vote_tuple_1, vote_tuple_2] + + self.assertEqual(vote_str_list, io.as_vote_strings(vote_str_list)) + self.assertEqual(vote_str_list, io.as_vote_strings(vote_list_list)) + self.assertEqual(vote_str_list, io.as_vote_strings(vote_tuple_list)) + + # mixing different representations of votes is confusing and therefore not + # recommended and forbidden by static type checking. + # However, we ensure the outcome is right nonetheless. + self.assertEqual( + 3*vote_str_list, + io.as_vote_strings([*vote_str_list, *vote_list_list, *vote_tuple_list]) # type:ignore + ) + + self.assertEqual(vote_tuple_list, io.as_vote_tuples(vote_str_list)) + if __name__ == '__main__': unittest.main() From ad5461288a9ad39385406259028a2bd2e3996150 Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Wed, 5 Jan 2022 17:14:57 +0100 Subject: [PATCH 4/6] io: use as_candidate in as_candidates --- schulze_condorcet/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schulze_condorcet/io.py b/schulze_condorcet/io.py index 707129d..ef2a7f1 100644 --- a/schulze_condorcet/io.py +++ b/schulze_condorcet/io.py @@ -21,7 +21,7 @@ def as_candidates(values: Sequence[str]) -> List[Candidate]: We respect the order of the candidates, as this is also respected during evaluation of the votes using the schulze method. """ - return [Candidate(value) for value in values] + return [as_candidate(value) for value in values] def as_vote_string( From 120425415ff134f48bcf00a79775c112ca3da4bc Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Wed, 5 Jan 2022 17:15:20 +0100 Subject: [PATCH 5/6] io: do not accept raw strings in as_vote_string instead, use VoteString directly for typing --- schulze_condorcet/io.py | 13 ++++--------- tests/test_schulze.py | 5 ++--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/schulze_condorcet/io.py b/schulze_condorcet/io.py index ef2a7f1..7775427 100644 --- a/schulze_condorcet/io.py +++ b/schulze_condorcet/io.py @@ -25,22 +25,17 @@ def as_candidates(values: Sequence[str]) -> List[Candidate]: def as_vote_string( - value: Union[str, VoteList, VoteTuple] + value: Union[VoteList, VoteTuple] ) -> VoteString: - """Convert a level-based representation of the vote into its string representation. - - This also accepts a string which will be marked as vote for convenience. - """ - if isinstance(value, str): - return VoteString(value) - elif isinstance(value, (list, tuple)): + """Convert a level-based representation of the vote into its string representation.""" + if isinstance(value, (list, tuple)): return VoteString(">".join("=".join(level) for level in value)) else: raise NotImplementedError(value) def as_vote_strings( - values: Union[Collection[str], Collection[VoteList], Collection[VoteTuple]] + values: Union[Collection[VoteList], Collection[VoteTuple]] ) -> List[VoteString]: """Convert each value into a string representation of the vote.""" return [as_vote_string(value) for value in values] diff --git a/tests/test_schulze.py b/tests/test_schulze.py index 449939e..a3d7532 100644 --- a/tests/test_schulze.py +++ b/tests/test_schulze.py @@ -534,7 +534,6 @@ def test_io(self) -> None: vote_list_list = [vote_list_1, vote_list_2] vote_tuple_list = [vote_tuple_1, vote_tuple_2] - self.assertEqual(vote_str_list, io.as_vote_strings(vote_str_list)) self.assertEqual(vote_str_list, io.as_vote_strings(vote_list_list)) self.assertEqual(vote_str_list, io.as_vote_strings(vote_tuple_list)) @@ -542,8 +541,8 @@ def test_io(self) -> None: # recommended and forbidden by static type checking. # However, we ensure the outcome is right nonetheless. self.assertEqual( - 3*vote_str_list, - io.as_vote_strings([*vote_str_list, *vote_list_list, *vote_tuple_list]) # type:ignore + 2*vote_str_list, + io.as_vote_strings([*vote_list_list, *vote_tuple_list]) # type:ignore ) self.assertEqual(vote_tuple_list, io.as_vote_tuples(vote_str_list)) From ff2b3f41f996f84925112ffe99245ea2c8f448b5 Mon Sep 17 00:00:00 2001 From: Lars Esser Date: Thu, 13 Jan 2022 17:32:49 +0100 Subject: [PATCH 6/6] util: rename io.py into util.py --- schulze_condorcet/schulze_condorcet.py | 2 +- schulze_condorcet/{io.py => util.py} | 0 tests/test_schulze.py | 14 +++++++------- 3 files changed, 8 insertions(+), 8 deletions(-) rename schulze_condorcet/{io.py => util.py} (100%) diff --git a/schulze_condorcet/schulze_condorcet.py b/schulze_condorcet/schulze_condorcet.py index 404337c..9feb5c9 100644 --- a/schulze_condorcet/schulze_condorcet.py +++ b/schulze_condorcet/schulze_condorcet.py @@ -4,7 +4,7 @@ Collection, Container, List, Mapping, Tuple, Sequence ) -from schulze_condorcet.io import as_vote_string, as_vote_tuples +from schulze_condorcet.util import as_vote_string, as_vote_tuples from schulze_condorcet.strength import winning_votes from schulze_condorcet.types import ( Candidate, DetailedResultLevel, LinkStrength, PairwisePreference, SchulzeResult, diff --git a/schulze_condorcet/io.py b/schulze_condorcet/util.py similarity index 100% rename from schulze_condorcet/io.py rename to schulze_condorcet/util.py diff --git a/tests/test_schulze.py b/tests/test_schulze.py index a3d7532..f341770 100644 --- a/tests/test_schulze.py +++ b/tests/test_schulze.py @@ -4,7 +4,7 @@ import unittest from schulze_condorcet import schulze_evaluate, schulze_evaluate_detailed -import schulze_condorcet.io as io +import schulze_condorcet.util as util from schulze_condorcet.strength import margin, winning_votes from schulze_condorcet.types import Candidate, DetailedResultLevel as DRL, VoteString @@ -518,10 +518,10 @@ def test_result_order(self) -> None: condensed = schulze_evaluate(reference_votes, candidates) self.assertEqual("1=0>2", condensed) - def test_io(self) -> None: + def test_util(self) -> None: candidates = ["1", "2", "3"] # This does only static type conversion - self.assertEqual(io.as_candidates(candidates), candidates) + self.assertEqual(util.as_candidates(candidates), candidates) # build the same votes, represented as string, list and tuple vote_str_1 = "1" @@ -534,18 +534,18 @@ def test_io(self) -> None: vote_list_list = [vote_list_1, vote_list_2] vote_tuple_list = [vote_tuple_1, vote_tuple_2] - self.assertEqual(vote_str_list, io.as_vote_strings(vote_list_list)) - self.assertEqual(vote_str_list, io.as_vote_strings(vote_tuple_list)) + self.assertEqual(vote_str_list, util.as_vote_strings(vote_list_list)) + self.assertEqual(vote_str_list, util.as_vote_strings(vote_tuple_list)) # mixing different representations of votes is confusing and therefore not # recommended and forbidden by static type checking. # However, we ensure the outcome is right nonetheless. self.assertEqual( 2*vote_str_list, - io.as_vote_strings([*vote_list_list, *vote_tuple_list]) # type:ignore + util.as_vote_strings([*vote_list_list, *vote_tuple_list]) # type:ignore ) - self.assertEqual(vote_tuple_list, io.as_vote_tuples(vote_str_list)) + self.assertEqual(vote_tuple_list, util.as_vote_tuples(vote_str_list)) if __name__ == '__main__':