kopia lustrzana https://github.com/martinlackner/apportionment
github actions; black formatting
rodzic
03673bf679
commit
4d1133ffb4
|
@ -0,0 +1,42 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
# TODO Can we make this workflow depend on unittests to avoid building broken packages?
|
||||||
|
# As it seems: no, we cannot. At least not really. Jobs can depend on other jobs, but workflows
|
||||||
|
# cannot depend on other workflows. But jobs cannot be triggered by events, only workflows are
|
||||||
|
# triggered by events. That means we would need to run the build/upload job for every commit
|
||||||
|
# and every Python version and then check if it is the right Python version and if there is a
|
||||||
|
# new tag and only then really upload a package.
|
||||||
|
# The only additional advantage of this approach might be that we could test the package
|
||||||
|
# building process for every commit and every Python version comfortably.
|
||||||
|
#
|
||||||
|
# Note: building the package is also tested in unittests.yml.
|
||||||
|
#
|
||||||
|
# https://github.community/t/how-do-i-specify-job-dependency-running-in-another-workflow/16482/2
|
||||||
|
# https://docs.github.com/en/actions/learn-github-actions/managing-complex-workflows#creating-dependent-jobs
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
|
||||||
|
- name: Upgrade pip
|
||||||
|
run: python -m pip install --upgrade pip
|
||||||
|
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
run: python setup.py bdist_wheel
|
||||||
|
|
||||||
|
- name: Publish distribution package to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@ -0,0 +1,72 @@
|
||||||
|
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
|
||||||
|
# For more information see:
|
||||||
|
# https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
|
||||||
|
|
||||||
|
name: Unittests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unittests:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.7, 3.8, 3.9, "3.10"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
# fetch all tags to be able to generate a version number for test packages
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -e .[dev]
|
||||||
|
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
pip install flake8
|
||||||
|
# stop the build if there are Python syntax errors or undefined names
|
||||||
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
|
|
||||||
|
- name: Check for correct black formatting
|
||||||
|
run: black --check .
|
||||||
|
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
# note that some tests are skipped because Gurobi is not available on Github
|
||||||
|
coverage run --source=. -m pytest -v --tb=long -m unittests.py
|
||||||
|
|
||||||
|
- name: Generate coverage report
|
||||||
|
run: coverage xml
|
||||||
|
|
||||||
|
- name: Upload coverage metrics to codecov
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
|
||||||
|
# tests the package, the release is done in build.yml
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
run: python setup.py bdist_wheel
|
||||||
|
env:
|
||||||
|
# does not increment on re-run... :(
|
||||||
|
# this means this will fail in the last step, when uploading the same package again to PyPI
|
||||||
|
# because PyPI requires unique file names for uploaded packages
|
||||||
|
BUILD_NUMBER: ${{ github.run_number }}
|
||||||
|
#
|
||||||
|
# # tests uploading the package to the test repo, but only if API_KEY is defined
|
||||||
|
# - name: Publish distribution package to Test PyPI
|
||||||
|
# env:
|
||||||
|
# TEST_PYPI_API_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||||
|
# if: (env.TEST_PYPI_API_TOKEN != null) && (matrix.python-version == '3.8') && (github.ref == 'refs/heads/master')
|
||||||
|
# uses: pypa/gh-action-pypi-publish@master
|
||||||
|
# with:
|
||||||
|
# password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||||
|
# repository_url: https://test.pypi.org/legacy/
|
|
@ -31,15 +31,13 @@ def compute(
|
||||||
parties=string.ascii_letters,
|
parties=string.ascii_letters,
|
||||||
threshold=None,
|
threshold=None,
|
||||||
tiesallowed=True,
|
tiesallowed=True,
|
||||||
verbose=True,
|
verbose=False,
|
||||||
):
|
):
|
||||||
filtered_votes = apply_threshold(votes, threshold)
|
filtered_votes = apply_threshold(votes, threshold)
|
||||||
if method == "quota":
|
if method == "quota":
|
||||||
return quota(filtered_votes, seats, fractions, parties, tiesallowed, verbose)
|
return quota(filtered_votes, seats, fractions, parties, tiesallowed, verbose)
|
||||||
elif method in ["lrm", "hamilton", "largest_remainder"]:
|
elif method in ["lrm", "hamilton", "largest_remainder"]:
|
||||||
return largest_remainder(
|
return largest_remainder(filtered_votes, seats, fractions, parties, tiesallowed, verbose)
|
||||||
filtered_votes, seats, fractions, parties, tiesallowed, verbose
|
|
||||||
)
|
|
||||||
elif method in [
|
elif method in [
|
||||||
"dhondt",
|
"dhondt",
|
||||||
"jefferson",
|
"jefferson",
|
||||||
|
@ -56,9 +54,7 @@ def compute(
|
||||||
"majorfractions",
|
"majorfractions",
|
||||||
"greatestdivisors",
|
"greatestdivisors",
|
||||||
]:
|
]:
|
||||||
return divisor(
|
return divisor(filtered_votes, seats, method, fractions, parties, tiesallowed, verbose)
|
||||||
filtered_votes, seats, method, fractions, parties, tiesallowed, verbose
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("apportionment method " + method + " not known")
|
raise NotImplementedError("apportionment method " + method + " not known")
|
||||||
|
|
||||||
|
@ -87,7 +83,7 @@ def __print_results(representatives, parties):
|
||||||
|
|
||||||
# verifies whether a given assignment of representatives
|
# verifies whether a given assignment of representatives
|
||||||
# is within quota
|
# is within quota
|
||||||
def within_quota(votes, representatives, parties=string.ascii_letters, verbose=True):
|
def within_quota(votes, representatives, parties=string.ascii_letters, verbose=False):
|
||||||
n = sum(votes)
|
n = sum(votes)
|
||||||
seats = sum(representatives)
|
seats = sum(representatives)
|
||||||
within = True
|
within = True
|
||||||
|
@ -128,7 +124,7 @@ def largest_remainder(
|
||||||
fractions=False,
|
fractions=False,
|
||||||
parties=string.ascii_letters,
|
parties=string.ascii_letters,
|
||||||
tiesallowed=True,
|
tiesallowed=True,
|
||||||
verbose=True,
|
verbose=False,
|
||||||
):
|
):
|
||||||
# votes = np.array(votes)
|
# votes = np.array(votes)
|
||||||
if verbose:
|
if verbose:
|
||||||
|
@ -136,9 +132,7 @@ def largest_remainder(
|
||||||
if fractions:
|
if fractions:
|
||||||
q = Fraction(int(sum(votes)), seats)
|
q = Fraction(int(sum(votes)), seats)
|
||||||
quotas = [Fraction(int(p), q) for p in votes]
|
quotas = [Fraction(int(p), q) for p in votes]
|
||||||
representatives = np.array(
|
representatives = np.array([int(qu.numerator // qu.denominator) for qu in quotas])
|
||||||
[int(qu.numerator // qu.denominator) for qu in quotas]
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
votes = np.array(votes)
|
votes = np.array(votes)
|
||||||
quotas = (votes * seats) / np.sum(votes)
|
quotas = (votes * seats) / np.sum(votes)
|
||||||
|
@ -186,7 +180,7 @@ def divisor(
|
||||||
fractions=False,
|
fractions=False,
|
||||||
parties=string.ascii_letters,
|
parties=string.ascii_letters,
|
||||||
tiesallowed=True,
|
tiesallowed=True,
|
||||||
verbose=True,
|
verbose=False,
|
||||||
):
|
):
|
||||||
votes = np.array(votes)
|
votes = np.array(votes)
|
||||||
representatives = np.zeros(len(votes), dtype=int)
|
representatives = np.zeros(len(votes), dtype=int)
|
||||||
|
@ -234,24 +228,17 @@ def divisor(
|
||||||
representatives = np.array([1 if p > 0 else 0 for p in votes])
|
representatives = np.array([1 if p > 0 else 0 for p in votes])
|
||||||
if fractions:
|
if fractions:
|
||||||
divisors = np.array(
|
divisors = np.array(
|
||||||
[
|
[Fraction(2 * (i + 1) * (i + 2), 2 * (i + 1) + 1) for i in range(seats)]
|
||||||
Fraction(2 * (i + 1) * (i + 2), 2 * (i + 1) + 1)
|
|
||||||
for i in range(seats)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
divisors = np.arange(seats)
|
divisors = np.arange(seats)
|
||||||
divisors = (2 * (divisors + 1) * (divisors + 2)) / (
|
divisors = (2 * (divisors + 1) * (divisors + 2)) / (2 * (divisors + 1) + 1)
|
||||||
2 * (divisors + 1) + 1
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("divisor method " + method + " not known")
|
raise NotImplementedError("divisor method " + method + " not known")
|
||||||
# assigning representatives
|
# assigning representatives
|
||||||
if seats > np.sum(representatives):
|
if seats > np.sum(representatives):
|
||||||
if fractions and method not in ["huntington", "hill", "modified_saintelague"]:
|
if fractions and method not in ["huntington", "hill", "modified_saintelague"]:
|
||||||
weights = np.array(
|
weights = np.array([[Fraction(int(p), d) for d in divisors.tolist()] for p in votes])
|
||||||
[[Fraction(int(p), d) for d in divisors.tolist()] for p in votes]
|
|
||||||
)
|
|
||||||
flatweights = sorted([w for l in weights for w in l])
|
flatweights = sorted([w for l in weights for w in l])
|
||||||
else:
|
else:
|
||||||
weights = np.array([p / divisors for p in votes])
|
weights = np.array([p / divisors for p in votes])
|
||||||
|
@ -296,11 +283,7 @@ def divisor(
|
||||||
def __divzero_fewerseatsthanparties(votes, seats, parties, tiesallowed, verbose):
|
def __divzero_fewerseatsthanparties(votes, seats, parties, tiesallowed, verbose):
|
||||||
representatives = np.zeros(len(votes), dtype=int)
|
representatives = np.zeros(len(votes), dtype=int)
|
||||||
if verbose:
|
if verbose:
|
||||||
print(
|
print(" fewer seats than parties; " + str(seats) + " strongest parties receive one seat")
|
||||||
" fewer seats than parties; "
|
|
||||||
+ str(seats)
|
|
||||||
+ " strongest parties receive one seat"
|
|
||||||
)
|
|
||||||
tiebreaking_message = " ties broken in favor of: "
|
tiebreaking_message = " ties broken in favor of: "
|
||||||
ties = False
|
ties = False
|
||||||
mincount = np.sort(votes)[-seats]
|
mincount = np.sort(votes)[-seats]
|
||||||
|
@ -328,7 +311,7 @@ def quota(
|
||||||
fractions=False,
|
fractions=False,
|
||||||
parties=string.ascii_letters,
|
parties=string.ascii_letters,
|
||||||
tiesallowed=True,
|
tiesallowed=True,
|
||||||
verbose=True,
|
verbose=False,
|
||||||
):
|
):
|
||||||
"""The quota method
|
"""The quota method
|
||||||
see Balinski, M. L., & Young, H. P. (1975).
|
see Balinski, M. L., & Young, H. P. (1975).
|
||||||
|
@ -338,9 +321,7 @@ def quota(
|
||||||
Warning: tiesallowed is not supported here (difficult to implement)
|
Warning: tiesallowed is not supported here (difficult to implement)
|
||||||
"""
|
"""
|
||||||
if not tiesallowed:
|
if not tiesallowed:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError("parameter tiesallowed not supported for Quota method")
|
||||||
"parameter tiesallowed not supported for Quota method"
|
|
||||||
)
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print("\nQuota method")
|
print("\nQuota method")
|
||||||
|
|
||||||
|
@ -350,8 +331,7 @@ def quota(
|
||||||
while np.sum(representatives) < seats:
|
while np.sum(representatives) < seats:
|
||||||
if fractions:
|
if fractions:
|
||||||
quotas = [
|
quotas = [
|
||||||
Fraction(int(votes[i]), int(representatives[i]) + 1)
|
Fraction(int(votes[i]), int(representatives[i]) + 1) for i in range(len(votes))
|
||||||
for i in range(len(votes))
|
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
quotas = votes / (representatives + 1)
|
quotas = votes / (representatives + 1)
|
||||||
|
|
|
@ -30,52 +30,41 @@ import apportionment.methods as app
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_all_implemented(method, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_all_implemented(method, fractions, verbose):
|
||||||
votes = [1]
|
votes = [1]
|
||||||
seats = 1
|
seats = 1
|
||||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [1]
|
assert app.compute(method, votes, seats, fractions=fractions, verbose=verbose) == [1]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", app.METHODS)
|
@pytest.mark.parametrize("method", app.METHODS)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_weak_proportionality(method, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_weak_proportionality(method, fractions, verbose):
|
||||||
votes = [14, 28, 7, 35]
|
votes = [14, 28, 7, 35]
|
||||||
seats = 12
|
seats = 12
|
||||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [
|
result = app.compute(method, votes, seats, fractions=fractions, verbose=verbose)
|
||||||
2,
|
assert result == [2, 4, 1, 5]
|
||||||
4,
|
|
||||||
1,
|
|
||||||
5,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", app.METHODS)
|
@pytest.mark.parametrize("method", app.METHODS)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_zero_parties(method, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_zero_parties(method, fractions, verbose):
|
||||||
votes = [0, 14, 28, 0, 0]
|
votes = [0, 14, 28, 0, 0]
|
||||||
seats = 6
|
seats = 6
|
||||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [
|
result = app.compute(method, votes, seats, fractions=fractions, verbose=verbose)
|
||||||
0,
|
assert result == [0, 2, 4, 0, 0]
|
||||||
2,
|
|
||||||
4,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", app.METHODS)
|
@pytest.mark.parametrize("method", app.METHODS)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_fewerseatsthanparties(method, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_fewerseatsthanparties(method, fractions, verbose):
|
||||||
votes = [10, 9, 8, 8, 11, 12]
|
votes = [10, 9, 8, 8, 11, 12]
|
||||||
seats = 3
|
seats = 3
|
||||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [
|
result = app.compute(method, votes, seats, fractions=fractions, verbose=verbose)
|
||||||
1,
|
assert result == [1, 0, 0, 0, 1, 1]
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# examples taken from
|
# examples taken from
|
||||||
|
@ -96,13 +85,11 @@ def test_fewerseatsthanparties(method, fractions):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_balinski_young_example1(method, expected, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_balinski_young_example1(method, expected, fractions, verbose):
|
||||||
votes = [5117, 4400, 162, 161, 160]
|
votes = [5117, 4400, 162, 161, 160]
|
||||||
seats = 100
|
seats = 100
|
||||||
assert (
|
assert app.compute(method, votes, seats, fractions=fractions, verbose=verbose) == expected
|
||||||
app.compute(method, votes, seats, fractions=fractions, verbose=False)
|
|
||||||
== expected
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -119,21 +106,20 @@ def test_balinski_young_example1(method, expected, fractions):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_balinski_young_example2(method, expected, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_balinski_young_example2(method, expected, fractions, verbose):
|
||||||
votes = [9061, 7179, 5259, 3319, 1182]
|
votes = [9061, 7179, 5259, 3319, 1182]
|
||||||
seats = 26
|
seats = 26
|
||||||
assert (
|
assert app.compute(method, votes, seats, fractions=fractions, verbose=verbose) == expected
|
||||||
app.compute(method, votes, seats, fractions=fractions, verbose=False)
|
|
||||||
== expected
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", app.METHODS)
|
@pytest.mark.parametrize("method", app.METHODS)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_tiebreaking(method, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_tiebreaking(method, fractions, verbose):
|
||||||
votes = [2, 1, 1, 2, 2]
|
votes = [2, 1, 1, 2, 2]
|
||||||
seats = 2
|
seats = 2
|
||||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [
|
assert app.compute(method, votes, seats, fractions=fractions, verbose=verbose) == [
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
@ -142,18 +128,20 @@ def test_tiebreaking(method, fractions):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_within_quota():
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_within_quota(verbose):
|
||||||
votes = [5117, 4400, 162, 161, 160]
|
votes = [5117, 4400, 162, 161, 160]
|
||||||
representatives = [51, 44, 2, 2, 1]
|
representatives = [51, 44, 2, 2, 1]
|
||||||
assert app.within_quota(votes, representatives, verbose=False)
|
assert app.within_quota(votes, representatives, verbose=verbose)
|
||||||
representatives = [52, 45, 1, 1, 1]
|
representatives = [52, 45, 1, 1, 1]
|
||||||
assert not app.within_quota(votes, representatives, verbose=False)
|
assert not app.within_quota(votes, representatives, verbose=verbose)
|
||||||
representatives = [52, 43, 2, 1, 2]
|
representatives = [52, 43, 2, 1, 2]
|
||||||
assert not app.within_quota(votes, representatives, verbose=False)
|
assert not app.within_quota(votes, representatives, verbose=verbose)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_threshold(fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_threshold(fractions, verbose):
|
||||||
votes = [41, 56, 3]
|
votes = [41, 56, 3]
|
||||||
seats = 60
|
seats = 60
|
||||||
threshold = 0.03
|
threshold = 0.03
|
||||||
|
@ -166,50 +154,49 @@ def test_threshold(fractions):
|
||||||
method = "dhondt"
|
method = "dhondt"
|
||||||
threshold = 0
|
threshold = 0
|
||||||
unfiltered_result = app.compute(
|
unfiltered_result = app.compute(
|
||||||
method, votes, seats, fractions=fractions, threshold=threshold, verbose=False
|
method, votes, seats, fractions=fractions, threshold=threshold, verbose=verbose
|
||||||
)
|
)
|
||||||
threshold = 0.04
|
threshold = 0.04
|
||||||
filtered_result = app.compute(
|
filtered_result = app.compute(
|
||||||
method, votes, seats, fractions=fractions, threshold=threshold, verbose=False
|
method, votes, seats, fractions=fractions, threshold=threshold, verbose=verbose
|
||||||
)
|
)
|
||||||
assert unfiltered_result != filtered_result
|
assert unfiltered_result != filtered_result
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_saintelague_difference(fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_saintelague_difference(fractions, verbose):
|
||||||
votes = [6, 1]
|
votes = [6, 1]
|
||||||
seats = 4
|
seats = 4
|
||||||
r1 = app.compute(
|
r1 = app.compute("saintelague", votes, seats, fractions=fractions, verbose=verbose) # [3, 1]
|
||||||
"saintelague", votes, seats, fractions=fractions, verbose=False
|
|
||||||
) # [3, 1]
|
|
||||||
r2 = app.compute(
|
r2 = app.compute(
|
||||||
"modified_saintelague", votes, seats, fractions=fractions, verbose=False
|
"modified_saintelague", votes, seats, fractions=fractions, verbose=verbose
|
||||||
) # [4, 0]
|
) # [4, 0]
|
||||||
assert r1 != r2
|
assert r1 != r2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", app.METHODS)
|
@pytest.mark.parametrize("method", app.METHODS)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_no_ties_allowed(method, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_no_ties_allowed(method, fractions, verbose):
|
||||||
votes = [11, 11, 11]
|
votes = [11, 11, 11]
|
||||||
seats = 4
|
seats = 4
|
||||||
if method == "quota":
|
if method == "quota":
|
||||||
return
|
return
|
||||||
with pytest.raises(app.TiesException):
|
with pytest.raises(app.TiesException):
|
||||||
app.compute(
|
app.compute(method, votes, seats, fractions=fractions, tiesallowed=False, verbose=verbose)
|
||||||
method, votes, seats, fractions=fractions, tiesallowed=False, verbose=False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", app.METHODS)
|
@pytest.mark.parametrize("method", app.METHODS)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_no_ties_allowed2(method, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_no_ties_allowed2(method, fractions, verbose):
|
||||||
votes = [12, 12, 11, 12]
|
votes = [12, 12, 11, 12]
|
||||||
seats = 3
|
seats = 3
|
||||||
if method == "quota":
|
if method == "quota":
|
||||||
return
|
return
|
||||||
assert app.compute(
|
assert app.compute(
|
||||||
method, votes, seats, fractions=fractions, tiesallowed=False, verbose=False
|
method, votes, seats, fractions=fractions, tiesallowed=False, verbose=verbose
|
||||||
) == [1, 1, 0, 1]
|
) == [1, 1, 0, 1]
|
||||||
|
|
||||||
|
|
||||||
|
@ -451,7 +438,8 @@ def test_no_ties_allowed2(method, fractions):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
def test_austrian_elections(year, partynames, votes, officialresult, fractions):
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
|
def test_austrian_elections(year, partynames, votes, officialresult, fractions, verbose):
|
||||||
result = app.compute(
|
result = app.compute(
|
||||||
"dhondt",
|
"dhondt",
|
||||||
votes,
|
votes,
|
||||||
|
@ -459,7 +447,7 @@ def test_austrian_elections(year, partynames, votes, officialresult, fractions):
|
||||||
fractions=fractions,
|
fractions=fractions,
|
||||||
parties=partynames,
|
parties=partynames,
|
||||||
threshold=0.04,
|
threshold=0.04,
|
||||||
verbose=True,
|
verbose=verbose,
|
||||||
)
|
)
|
||||||
assert str(tuple(result) == tuple(officialresult))
|
assert str(tuple(result) == tuple(officialresult))
|
||||||
|
|
||||||
|
@ -556,8 +544,9 @@ def test_austrian_elections(year, partynames, votes, officialresult, fractions):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize("fractions", [True, False])
|
@pytest.mark.parametrize("fractions", [True, False])
|
||||||
|
@pytest.mark.parametrize("verbose", [True, False])
|
||||||
def test_israeli_elections(
|
def test_israeli_elections(
|
||||||
knesset_nr, partynames, votes, officialresult, threshold, fractions
|
knesset_nr, partynames, votes, officialresult, threshold, fractions, verbose
|
||||||
):
|
):
|
||||||
print("Knesset #" + str(knesset_nr) + ":")
|
print("Knesset #" + str(knesset_nr) + ":")
|
||||||
result = app.compute(
|
result = app.compute(
|
||||||
|
@ -567,6 +556,6 @@ def test_israeli_elections(
|
||||||
fractions=fractions,
|
fractions=fractions,
|
||||||
parties=partynames,
|
parties=partynames,
|
||||||
threshold=threshold,
|
threshold=threshold,
|
||||||
verbose=True,
|
verbose=verbose,
|
||||||
)
|
)
|
||||||
assert str(tuple(result) == tuple(officialresult))
|
assert str(tuple(result) == tuple(officialresult))
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
[build-system]
|
||||||
|
requires = [
|
||||||
|
"setuptools>=42",
|
||||||
|
"wheel"
|
||||||
|
]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
# https://docs.pytest.org/en/stable/customize.html
|
||||||
|
addopts = '''
|
||||||
|
-ra
|
||||||
|
--tb=line
|
||||||
|
-W error
|
||||||
|
--doctest-modules
|
||||||
|
--strict-markers
|
||||||
|
'''
|
||||||
|
# -ra: show extra test summary [...] (a)ll except passed [...]
|
||||||
|
# --tb=style: traceback print mode (auto/long/short/line/native/no)
|
||||||
|
# -W: set which warnings to report, "error" turns matching warnings into exceptions
|
||||||
|
|
||||||
|
testpaths = [
|
||||||
|
"tests",
|
||||||
|
]
|
||||||
|
|
||||||
|
norecursedirs = [
|
||||||
|
".git",
|
||||||
|
".github",
|
||||||
|
".pytest_cache",
|
||||||
|
"__pycache__",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
# https://coverage.readthedocs.io/en/latest/config.html
|
||||||
|
branch = true
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
show_missing = true
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
# https://black.readthedocs.io/en/stable/pyproject_toml.html
|
||||||
|
line-length = 99
|
||||||
|
target-version = ['py38']
|
||||||
|
exclude = '''
|
||||||
|
(
|
||||||
|
/(
|
||||||
|
\.git
|
||||||
|
| \.github
|
||||||
|
| \.pytest_cache
|
||||||
|
| env
|
||||||
|
| venv
|
||||||
|
)/
|
||||||
|
)
|
||||||
|
'''
|
|
@ -0,0 +1,103 @@
|
||||||
|
import os
|
||||||
|
import setuptools
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def read_readme():
|
||||||
|
with open("README.md", "r", encoding="utf-8") as fh:
|
||||||
|
readme = fh.read()
|
||||||
|
return readme
|
||||||
|
|
||||||
|
|
||||||
|
def read_version():
|
||||||
|
"""This is not guaranteed to be a valid version string, but should work well enough.
|
||||||
|
|
||||||
|
Tested with version strings as tag names of the following formats:
|
||||||
|
|
||||||
|
2.0.0
|
||||||
|
2.0.0-beta
|
||||||
|
v2.0.0
|
||||||
|
v2.0.0-beta
|
||||||
|
|
||||||
|
Version strings need to comply with PEP 440. Git tags are used, but for development versions
|
||||||
|
the build number is appended. To comply with PEP 440 everything after the first dash is removed
|
||||||
|
before appending the build number.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
git_describe = subprocess.run(
|
||||||
|
["git", "describe", "--tags", "--abbrev=0"], stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
if git_describe.returncode == 0:
|
||||||
|
git_version = git_describe.stdout.strip().decode("utf-8")
|
||||||
|
else:
|
||||||
|
# per default no old commit are fetched in Github actions, that means, that git describe
|
||||||
|
# fails if the latest commit is not a tag, because old tags can't be found.
|
||||||
|
# to fetch tags in Github actions use "fetch-depth: 0"
|
||||||
|
raise RuntimeError("Git describe failed: did you fetch at least one tag?")
|
||||||
|
|
||||||
|
# git_version contains the latest tag, if it is not identical with HEAD need to postifx
|
||||||
|
head_is_tag = (
|
||||||
|
subprocess.run(
|
||||||
|
["git", "describe", "--tags", "--exact-match", "HEAD"], stderr=subprocess.PIPE
|
||||||
|
).returncode
|
||||||
|
== 0
|
||||||
|
)
|
||||||
|
if not head_is_tag:
|
||||||
|
try:
|
||||||
|
# set by Github actions, necessary for unique file names for PyPI
|
||||||
|
build_nr = os.environ["BUILD_NUMBER"]
|
||||||
|
except KeyError:
|
||||||
|
build_nr = 0
|
||||||
|
|
||||||
|
# +something is disallowed on PyPI even if allowed in PEP 440... :-/
|
||||||
|
# https://github.com/pypa/pypi-legacy/issues/731#issuecomment-345461596
|
||||||
|
|
||||||
|
next_stable = git_version.split("-")[0]
|
||||||
|
git_version = f"{next_stable}.dev{build_nr}"
|
||||||
|
|
||||||
|
if git_version[0] == "v":
|
||||||
|
git_version = git_version[1:]
|
||||||
|
|
||||||
|
return git_version
|
||||||
|
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name="abcvoting",
|
||||||
|
version=read_version(),
|
||||||
|
author="Martin Lackner",
|
||||||
|
author_email="unexpected@sent.at",
|
||||||
|
description="A Python implementation of common apportionment methods",
|
||||||
|
long_description=read_readme(),
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
url="https://github.com/martinlackner/apportionment/",
|
||||||
|
project_urls={"Bug Tracker": "https://github.com/martinlackner/apportionment/issues"},
|
||||||
|
license="MIT License",
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.7",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Intended Audience :: Science/Research",
|
||||||
|
],
|
||||||
|
packages=["apportionment"],
|
||||||
|
python_requires=">=3.7",
|
||||||
|
setup_requires=[
|
||||||
|
"wheel",
|
||||||
|
],
|
||||||
|
install_requires=[
|
||||||
|
"numpy>=2.2",
|
||||||
|
],
|
||||||
|
extras_require={
|
||||||
|
"dev": [
|
||||||
|
"pytest>=6",
|
||||||
|
"coverage[toml]>=5.3",
|
||||||
|
"black==22.1.0",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
Ładowanie…
Reference in New Issue