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,
|
||||
threshold=None,
|
||||
tiesallowed=True,
|
||||
verbose=True,
|
||||
verbose=False,
|
||||
):
|
||||
filtered_votes = apply_threshold(votes, threshold)
|
||||
if method == "quota":
|
||||
return quota(filtered_votes, seats, fractions, parties, tiesallowed, verbose)
|
||||
elif method in ["lrm", "hamilton", "largest_remainder"]:
|
||||
return largest_remainder(
|
||||
filtered_votes, seats, fractions, parties, tiesallowed, verbose
|
||||
)
|
||||
return largest_remainder(filtered_votes, seats, fractions, parties, tiesallowed, verbose)
|
||||
elif method in [
|
||||
"dhondt",
|
||||
"jefferson",
|
||||
|
@ -56,9 +54,7 @@ def compute(
|
|||
"majorfractions",
|
||||
"greatestdivisors",
|
||||
]:
|
||||
return divisor(
|
||||
filtered_votes, seats, method, fractions, parties, tiesallowed, verbose
|
||||
)
|
||||
return divisor(filtered_votes, seats, method, fractions, parties, tiesallowed, verbose)
|
||||
else:
|
||||
raise NotImplementedError("apportionment method " + method + " not known")
|
||||
|
||||
|
@ -87,7 +83,7 @@ def __print_results(representatives, parties):
|
|||
|
||||
# verifies whether a given assignment of representatives
|
||||
# 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)
|
||||
seats = sum(representatives)
|
||||
within = True
|
||||
|
@ -128,7 +124,7 @@ def largest_remainder(
|
|||
fractions=False,
|
||||
parties=string.ascii_letters,
|
||||
tiesallowed=True,
|
||||
verbose=True,
|
||||
verbose=False,
|
||||
):
|
||||
# votes = np.array(votes)
|
||||
if verbose:
|
||||
|
@ -136,9 +132,7 @@ def largest_remainder(
|
|||
if fractions:
|
||||
q = Fraction(int(sum(votes)), seats)
|
||||
quotas = [Fraction(int(p), q) for p in votes]
|
||||
representatives = np.array(
|
||||
[int(qu.numerator // qu.denominator) for qu in quotas]
|
||||
)
|
||||
representatives = np.array([int(qu.numerator // qu.denominator) for qu in quotas])
|
||||
else:
|
||||
votes = np.array(votes)
|
||||
quotas = (votes * seats) / np.sum(votes)
|
||||
|
@ -186,7 +180,7 @@ def divisor(
|
|||
fractions=False,
|
||||
parties=string.ascii_letters,
|
||||
tiesallowed=True,
|
||||
verbose=True,
|
||||
verbose=False,
|
||||
):
|
||||
votes = np.array(votes)
|
||||
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])
|
||||
if fractions:
|
||||
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:
|
||||
divisors = np.arange(seats)
|
||||
divisors = (2 * (divisors + 1) * (divisors + 2)) / (
|
||||
2 * (divisors + 1) + 1
|
||||
)
|
||||
divisors = (2 * (divisors + 1) * (divisors + 2)) / (2 * (divisors + 1) + 1)
|
||||
else:
|
||||
raise NotImplementedError("divisor method " + method + " not known")
|
||||
# assigning representatives
|
||||
if seats > np.sum(representatives):
|
||||
if fractions and method not in ["huntington", "hill", "modified_saintelague"]:
|
||||
weights = np.array(
|
||||
[[Fraction(int(p), d) for d in divisors.tolist()] for p in votes]
|
||||
)
|
||||
weights = np.array([[Fraction(int(p), d) for d in divisors.tolist()] for p in votes])
|
||||
flatweights = sorted([w for l in weights for w in l])
|
||||
else:
|
||||
weights = np.array([p / divisors for p in votes])
|
||||
|
@ -296,11 +283,7 @@ def divisor(
|
|||
def __divzero_fewerseatsthanparties(votes, seats, parties, tiesallowed, verbose):
|
||||
representatives = np.zeros(len(votes), dtype=int)
|
||||
if verbose:
|
||||
print(
|
||||
" fewer seats than parties; "
|
||||
+ str(seats)
|
||||
+ " strongest parties receive one seat"
|
||||
)
|
||||
print(" fewer seats than parties; " + str(seats) + " strongest parties receive one seat")
|
||||
tiebreaking_message = " ties broken in favor of: "
|
||||
ties = False
|
||||
mincount = np.sort(votes)[-seats]
|
||||
|
@ -328,7 +311,7 @@ def quota(
|
|||
fractions=False,
|
||||
parties=string.ascii_letters,
|
||||
tiesallowed=True,
|
||||
verbose=True,
|
||||
verbose=False,
|
||||
):
|
||||
"""The quota method
|
||||
see Balinski, M. L., & Young, H. P. (1975).
|
||||
|
@ -338,9 +321,7 @@ def quota(
|
|||
Warning: tiesallowed is not supported here (difficult to implement)
|
||||
"""
|
||||
if not tiesallowed:
|
||||
raise NotImplementedError(
|
||||
"parameter tiesallowed not supported for Quota method"
|
||||
)
|
||||
raise NotImplementedError("parameter tiesallowed not supported for Quota method")
|
||||
if verbose:
|
||||
print("\nQuota method")
|
||||
|
||||
|
@ -350,8 +331,7 @@ def quota(
|
|||
while np.sum(representatives) < seats:
|
||||
if fractions:
|
||||
quotas = [
|
||||
Fraction(int(votes[i]), int(representatives[i]) + 1)
|
||||
for i in range(len(votes))
|
||||
Fraction(int(votes[i]), int(representatives[i]) + 1) for i in range(len(votes))
|
||||
]
|
||||
else:
|
||||
quotas = votes / (representatives + 1)
|
||||
|
|
|
@ -30,52 +30,41 @@ import apportionment.methods as app
|
|||
],
|
||||
)
|
||||
@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]
|
||||
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("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]
|
||||
seats = 12
|
||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [
|
||||
2,
|
||||
4,
|
||||
1,
|
||||
5,
|
||||
]
|
||||
result = app.compute(method, votes, seats, fractions=fractions, verbose=verbose)
|
||||
assert result == [2, 4, 1, 5]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", app.METHODS)
|
||||
@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]
|
||||
seats = 6
|
||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
result = app.compute(method, votes, seats, fractions=fractions, verbose=verbose)
|
||||
assert result == [0, 2, 4, 0, 0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", app.METHODS)
|
||||
@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]
|
||||
seats = 3
|
||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
]
|
||||
result = app.compute(method, votes, seats, fractions=fractions, verbose=verbose)
|
||||
assert result == [1, 0, 0, 0, 1, 1]
|
||||
|
||||
|
||||
# examples taken from
|
||||
|
@ -96,13 +85,11 @@ def test_fewerseatsthanparties(method, fractions):
|
|||
],
|
||||
)
|
||||
@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]
|
||||
seats = 100
|
||||
assert (
|
||||
app.compute(method, votes, seats, fractions=fractions, verbose=False)
|
||||
== expected
|
||||
)
|
||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=verbose) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -119,21 +106,20 @@ def test_balinski_young_example1(method, expected, fractions):
|
|||
],
|
||||
)
|
||||
@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]
|
||||
seats = 26
|
||||
assert (
|
||||
app.compute(method, votes, seats, fractions=fractions, verbose=False)
|
||||
== expected
|
||||
)
|
||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=verbose) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", app.METHODS)
|
||||
@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]
|
||||
seats = 2
|
||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=False) == [
|
||||
assert app.compute(method, votes, seats, fractions=fractions, verbose=verbose) == [
|
||||
1,
|
||||
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]
|
||||
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]
|
||||
assert not app.within_quota(votes, representatives, verbose=False)
|
||||
assert not app.within_quota(votes, representatives, verbose=verbose)
|
||||
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])
|
||||
def test_threshold(fractions):
|
||||
@pytest.mark.parametrize("verbose", [True, False])
|
||||
def test_threshold(fractions, verbose):
|
||||
votes = [41, 56, 3]
|
||||
seats = 60
|
||||
threshold = 0.03
|
||||
|
@ -166,50 +154,49 @@ def test_threshold(fractions):
|
|||
method = "dhondt"
|
||||
threshold = 0
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
@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]
|
||||
seats = 4
|
||||
r1 = app.compute(
|
||||
"saintelague", votes, seats, fractions=fractions, verbose=False
|
||||
) # [3, 1]
|
||||
r1 = app.compute("saintelague", votes, seats, fractions=fractions, verbose=verbose) # [3, 1]
|
||||
r2 = app.compute(
|
||||
"modified_saintelague", votes, seats, fractions=fractions, verbose=False
|
||||
"modified_saintelague", votes, seats, fractions=fractions, verbose=verbose
|
||||
) # [4, 0]
|
||||
assert r1 != r2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", app.METHODS)
|
||||
@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]
|
||||
seats = 4
|
||||
if method == "quota":
|
||||
return
|
||||
with pytest.raises(app.TiesException):
|
||||
app.compute(
|
||||
method, votes, seats, fractions=fractions, tiesallowed=False, verbose=False
|
||||
)
|
||||
app.compute(method, votes, seats, fractions=fractions, tiesallowed=False, verbose=verbose)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", app.METHODS)
|
||||
@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]
|
||||
seats = 3
|
||||
if method == "quota":
|
||||
return
|
||||
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]
|
||||
|
||||
|
||||
|
@ -451,7 +438,8 @@ def test_no_ties_allowed2(method, fractions):
|
|||
],
|
||||
)
|
||||
@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(
|
||||
"dhondt",
|
||||
votes,
|
||||
|
@ -459,7 +447,7 @@ def test_austrian_elections(year, partynames, votes, officialresult, fractions):
|
|||
fractions=fractions,
|
||||
parties=partynames,
|
||||
threshold=0.04,
|
||||
verbose=True,
|
||||
verbose=verbose,
|
||||
)
|
||||
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("verbose", [True, False])
|
||||
def test_israeli_elections(
|
||||
knesset_nr, partynames, votes, officialresult, threshold, fractions
|
||||
knesset_nr, partynames, votes, officialresult, threshold, fractions, verbose
|
||||
):
|
||||
print("Knesset #" + str(knesset_nr) + ":")
|
||||
result = app.compute(
|
||||
|
@ -567,6 +556,6 @@ def test_israeli_elections(
|
|||
fractions=fractions,
|
||||
parties=partynames,
|
||||
threshold=threshold,
|
||||
verbose=True,
|
||||
verbose=verbose,
|
||||
)
|
||||
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