Skip to content

Commit 14965cb

Browse files
authored
Added testing for Schulze functions. (#104)
* Added pandas and pytest to requirements.txt * updated module name in test_main.py * Wrote tests for schulze * Removed dead code * Updated license * Updated dates
1 parent 27f236d commit 14965cb

File tree

9 files changed

+192
-112
lines changed

9 files changed

+192
-112
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ build/
1818

1919
**/*/node_modules
2020

21-
/meta/
21+
/meta/
22+
23+
test/*.db
24+
test/meta

elekto/core/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
"""
2121

2222
from collections import defaultdict
23+
from typing import Dict, List, Tuple
24+
from .types import BallotType
2325

24-
25-
def schulze_d(candidates, ballots):
26+
# Higher rank numbers receive higher preference
27+
def schulze_d(candidates: List[str], ballots: BallotType):
2628
d = {(V, W): 0 for V in candidates for W in candidates if V != W}
2729
for voter in ballots.keys():
2830
for V, Vr in ballots[voter]:
@@ -33,7 +35,7 @@ def schulze_d(candidates, ballots):
3335
return d
3436

3537

36-
def schulze_p(candidates, d):
38+
def schulze_p(candidates: List[str], d: Dict[Tuple[str, str], int]):
3739
p = {}
3840
for X in candidates:
3941
for Y in candidates:
@@ -52,7 +54,7 @@ def schulze_p(candidates, d):
5254
return p
5355

5456

55-
def schulze_rank(candidates, p, no_winners=1):
57+
def schulze_rank(candidates: List[str], p: Dict[Tuple[str, str], int], no_winners=1):
5658
wins = defaultdict(list)
5759

5860
for V in candidates:

elekto/core/election.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,23 @@
1414
#
1515
# Author(s): Manish Sahani <rec.manish.sahani@gmail.com>
1616

17+
from typing import List
18+
from .types import BallotType
19+
from pandas import DataFrame
1720
from elekto.core import schulze_d, schulze_p, schulze_rank
1821

19-
2022
class Election:
21-
def __init__(self, candidates, ballots):
23+
def __init__(self, candidates: List[str], ballots: BallotType, no_winners=1):
2224
self.candidates = candidates
2325
self.ballots = ballots
26+
self.no_winners = no_winners
2427
self.d = {}
2528
self.p = {}
2629

2730
def schulze(self):
2831
self.d = schulze_d(self.candidates, self.ballots)
2932
self.p = schulze_p(self.candidates, self.d)
30-
self.ranks = schulze_rank(self.candidates, self.p)
33+
self.ranks = schulze_rank(self.candidates, self.p, self.no_winners)
3134

3235
return self
3336

@@ -46,9 +49,9 @@ def build(candidates, ballots):
4649
return Election(candidates, pref)
4750

4851
@ staticmethod
49-
def from_csv(df, no_winners):
52+
def from_csv(df: DataFrame, no_winners: int):
5053
candidates = list(df.columns)
51-
ballots = {}
54+
ballots: BallotType = {}
5255

5356
for v, row in df.iterrows():
5457
ballots[v] = []

elekto/core/types.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2024 The Elekto Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# Author(s): Carson Weeks <mail@carsonweeks.com>
16+
17+
from typing import Dict, List, Tuple, TypeAlias
18+
19+
BallotType: TypeAlias = Dict[int, List[Tuple[str, int]]]

requirements.txt

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
1-
Flask>=2.0.3
2-
python-dotenv>=0.19.2
3-
requests>=2.27.1
4-
PyYAML>=6.0
5-
authlib>=0.15.5
6-
SQLAlchemy==1.4.49
7-
mysqlclient>=2.1.0
8-
psycopg2>=2.9.3
9-
markdown2>=2.4.2
10-
uwsgi>=2.0.20
11-
Flask-WTF>=1.0.0
12-
PyNaCl>=1.5.0
1+
authlib==1.4.0
2+
blinker==1.9.0
3+
certifi==2024.12.14
4+
cffi==1.17.1
5+
charset-normalizer==3.4.1
6+
click==8.1.8
7+
cryptography==44.0.0
8+
flask==3.1.0
9+
flask-wtf==1.2.2
10+
greenlet==3.1.1
11+
idna==3.10
12+
iniconfig==2.0.0
13+
itsdangerous==2.2.0
14+
jinja2==3.1.5
15+
markdown2==2.5.2
16+
markupsafe==3.0.2
17+
mysqlclient==2.2.7
18+
numpy==2.2.2
19+
packaging==24.2
20+
pandas==2.2.3
21+
pluggy==1.5.0
22+
psycopg2==2.9.10
23+
pycparser==2.22
24+
pynacl==1.5.0
25+
pytest==8.3.4
26+
python-dateutil==2.9.0.post0
27+
python-dotenv==1.0.1
28+
pytz==2024.2
29+
pyyaml==6.0.2
30+
requests==2.32.3
31+
six==1.17.0
32+
sqlalchemy==1.4.49
33+
tzdata==2025.1
34+
urllib3==2.3.0
35+
uwsgi==2.0.28
36+
werkzeug==3.1.3
37+
wtforms==3.2.1

test/.gitignore

Lines changed: 0 additions & 2 deletions
This file was deleted.

test/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2025 The Elekto Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# Author(s): Carson Weeks <mail@carsonweeks.com>

test/test_core_init.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright 2025 The Elekto Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# Author(s): Carson Weeks <mail@carsonweeks.com>
16+
17+
import os
18+
import sys
19+
import pytest
20+
21+
from elekto.core import schulze_d, schulze_p, schulze_rank # noqa
22+
23+
def test_schulze_d():
24+
candidates = ["A", "B", "C"]
25+
ballots = {
26+
"voter1": [("A", 3), ("B", 2), ("C", 1)],
27+
"voter2": [("B", 3), ("C", 2), ("A", 1)]
28+
}
29+
30+
expected_d = {
31+
("A", "B"): 1,
32+
("A", "C"): 1,
33+
("B", "A"): 1,
34+
("B", "C"): 2,
35+
("C", "A"): 1,
36+
("C", "B"): 0,
37+
}
38+
39+
result = schulze_d(candidates, ballots)
40+
print(result)
41+
assert result == expected_d
42+
43+
def test_schulze_p():
44+
candidates = ["A", "B", "C", "D"]
45+
d = {
46+
("A", "B"): 12, ("B", "A"): 9,
47+
("A", "C"): 7, ("C", "A"): 14,
48+
("A", "D"): 16, ("D", "A"): 3,
49+
("B", "C"): 5, ("C", "B"): 10,
50+
("B", "D"): 18, ("D", "B"): 1,
51+
("C", "D"): 2, ("D", "C"): 20,
52+
}
53+
54+
expected_p = {
55+
("A", "B"): 12,
56+
("B", "A"): 14,
57+
("A", "C"): 16,
58+
("C", "A"): 14,
59+
("A", "D"): 16,
60+
("D", "A"): 14,
61+
("B", "C"): 18,
62+
("C", "B"): 12,
63+
("B", "D"): 18,
64+
("D", "B"): 12,
65+
("C", "D"): 14,
66+
("D", "C"): 20,
67+
}
68+
69+
result = schulze_p(candidates, d)
70+
print(result)
71+
assert result == expected_p
72+
73+
def test_schulze_rank_simple():
74+
candidates = ["A", "B", "C"]
75+
p = {
76+
("A", "B"): 10, ("B", "A"): 5,
77+
("A", "C"): 15, ("C", "A"): 2,
78+
("B", "C"): 8, ("C", "B"): 3,
79+
}
80+
81+
expected = [
82+
(0, ["C"]),
83+
(1, ["B"]),
84+
(2, ["A"])
85+
]
86+
87+
result = schulze_rank(candidates, p)
88+
assert result == expected
89+
90+
def test_schulze_rank_tie():
91+
candidates = ["A", "B", "C"]
92+
p = {
93+
("A", "B"): 10, ("B", "A"): 5,
94+
("B", "C"): 10, ("C", "B"): 5,
95+
("C", "A"): 10, ("A", "C"): 5,
96+
}
97+
expected = [
98+
(1, ["A", "B", "C"])
99+
]
100+
101+
result = schulze_rank(candidates, p)
102+
assert result == expected
103+

test/test_main.py

Lines changed: 0 additions & 88 deletions
This file was deleted.

0 commit comments

Comments
 (0)