Repository navigation
Upgrade dependencies to latest stable and modernize for Python 3.11 - #825
Open
ebubekirrcihangirr6060 wants to merge 23 commits into
Open
ebubekirrcihangirr6060 wants to merge 23 commits into
ebubekirrcihangirr6060 wants to merge 23 commits into
Conversation
- Bump all runtime and dev dependencies to their latest stable releases (pydantic 2.14, mypy 2.4, black 26.10, ipython 9, ipykernel 7, pymongo 4.19, ...) - Raise requires-python to >=3.11 (3.9/3.10 are EOL and several latest releases need 3.11) - Move pytest-mock and types-python-dateutil from runtime to dev dependencies - Bump uv_build backend to 0.13 and CI Python to 3.11 - Disable pylint import-self (false positive on mkinit __init__ files in pylint 4.1) Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
- Use PEP 585/604 annotations (list[...], X | None) and collections.abc imports - Use datetime.UTC, drop redundant open modes and super() arguments - Fix Singleton: keep one instance per subclass and type get_instance as Self (previously all subclasses shared a single slot and returned the base instance) - Simplify redundant comprehensions, nested ifs and boolean returns - Move broad-exception pylint pragmas inline (pylint 4.1 no longer applies the preceding-line form to the except clause) and use the current message name - Make the utc_to_str test independent of the local timezone Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
- checkout/setup-python v7, upload-artifact v7, codeql-action v4, retry v4, setup-uv v10.3.0 (codeql-action@v2 and upload-artifact@v2 are retired, so CI could not run) - Replace archived create-release/upload-release-asset with softprops/action-gh-release@v3 Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
- DiscreteLog: use get_instance() so set_max_exponent/set_lazy_evaluation take effect; precompute_cache_async now fills the instance cache instead of a module-level dict - Scheduler: stop opening pools twice in __enter__, make close() idempotent and add use_scheduler() so tally/decryption close the schedulers they create - ReadOnlyDataStore: follow the Mapping contract (iterate keys, raise KeyError) and remove the self-recursive __eq__ - Add regression tests for each defect Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
ebubekirrcihangirr6060
force-pushed
the
chore/upgrade-deps-refactor
branch
from
October 22, 2026 16:48
183877f to
d1c754a
Compare
… pip install electionguard_tools is shipped in the package and imported by the eg CLI, and its factories import hypothesis, which was only a dev dependency. A clean 'pip install electionguard' therefore failed with ModuleNotFoundError on any eg command. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
The decrypt step only announced the present guardians and never announced the missing ones or exchanged compensated shares, so import-ballots with fewer guardian keys than the guardian count crashed with 'Unwrap called on None', even when the quorum was met. - Announce missing guardians and have the present ones compensate for them, using every guardian key exchanged during the key ceremony - Fail with a clear message when fewer guardians than the quorum are present Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
The election record only contained records for the guardians present at decryption, so verifiers could not check the recovery keys of compensated decryption shares against the missing guardian's commitments. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
encrypt.py and elgamal.py logged every ballot, selection and ciphertext at INFO, which the default console handler prints: encrypting the six sample ballots printed 288 lines (about 280 KB) of hex and raw bytes. The traces only contain public values, so this is noise rather than a leak; they now log at DEBUG. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
negate_q returned q - a without reducing mod q, so negate_q(0) produced q itself and ElementModQ raised OverflowError. The disjunctive Chaum-Pedersen proof calls negate_q on a random nonce, so encryption failed whenever that nonce was 0. With the production q this is practically never hit, but the test constants use q = 65521, which made property tests such as test_get_plaintext_tally_with_all_guardians_present fail at random (seen on CI as a Hypothesis FlakyFailure). The result is now reduced mod q. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
pydantic 2.14.2, python-dateutil 4.0.0, hypothesis 6.170.14, coverage 7.17.2, ipython 9.18.1, mkdocs-jupyter 0.27 (0.26.3 is kept where 0.27 does not support the Python version), mypy 2.4.1, pytest 9.2.0, pytest-mock 3.17.0 and types-python-dateutil 2.9.0.20261104. python-dateutil 4 drops the six dependency; electionguard only uses dateutil.parser.parse for datetimes in serialized records, and records written with this version still verify with the previous release. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
pydantic 2.14.3 (pydantic-core 2.50.4), click 8.5.1 and hypothesis 6.170.15. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
test_encrypt_contest_overvote_fails has been skipped as "runs forever" since 2022. It no longer hangs, but it expected encrypt_contest to return None for an overvote, while encrypt_contest now encrypts the contest with no votes counted and records the overvote in the encrypted contest data. Its generated descriptions could also be invalid, so it failed before reaching encryption. The new test checks that an overvoted contest has valid proofs, decrypts to zero for every real selection, fills the placeholder, and carries the OverVote error with the selected ids. No other test covered this path through encrypt_contest. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
A start at or above Q looped forever, and a start just below the 256-bit Q needed about 2^256 draws. A start of 0 skipped the loop and always returned 0. The value is now start + randbelow(q - start), and a start with no valid values raises ValueError. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
A wrong key or corrupted data gives a value that is not a small power of g. The search then took up to 100 million steps and stored each one, estimated at about 42 minutes and 214 GB. A baby-step giant-step check now rejects such values in under a second (measured at 0.37 s and 22 MB) without touching the cache. decrypt_selection_with_decryption_shares returns None for them, as documented. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
get_contest_description_well_formed could draw the same email twice, giving duplicate candidate ids and an invalid description, so property tests failed at random. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
eel 0.18.2 uses names pyparsing has deprecated, and only eel can change that. This hides those warnings in pytest rather than fixing them. The filter is limited to DeprecationWarning from module eel. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
The results were correct, but on a slow or busy machine some examples ran past their 2-15 s deadline and failed with DeadlineExceeded, including under coverage on Python 3.11. Timing is not what these tests check, and CI still has an overall timeout. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
The baby-step giant-step search already finds the exponent, so a value more than 100,000 steps past the cache now stores only its own entry. Walking to 50 million would have stored 50 million entries (about 107 GB at the measured rate); it now takes about 0.1 s and no extra memory. Nearby values still fill the cache as before. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
Nonces produced values in [0,Q) while elgamal_encrypt requires [1,Q), so a hash of zero made encryption return None. With the 16 bit test Q this happens about once in 65,521 draws and made tests fail at random, in their setUp. A zero is now replaced by a deterministic re-hash, so encryption and decryption derive the same value. With the production Q a zero is practically never hashed, so existing nonces and records do not change. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
The earlier change only removed the deadlines that tests set themselves. Tests without their own settings still had Hypothesis's default 200 ms per example, so test_elgamal_encryption_decryption_with_known_nonce_inverses failed with DeadlineExceeded on Python 3.15.2 while the machine was busy, although its result was correct. A profile in tests/conftest.py now sets deadline=None for the session, and per-test settings inherit it. A test checks the default. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
Eel 0.18.2 calls pyparsing's deprecated nestedExpr and excludeChars when it is imported, so every import of the GUI printed DeprecationWarnings, which pytest only hid. The Eel repository is archived, so the fix cannot be released there. Eel 0.18.2 (MIT) is now copied into src/electionguard_eel, unchanged except for: imports renamed to electionguard_eel, eel.js found through __name__, nested_expr / exclude_chars instead of the deprecated names, stdlib importlib.resources instead of the importlib_resources backport, and no PyInstaller command line. The GUI imports it as eel, so its code is otherwise unchanged. Its dependencies are listed directly; eel, future and importlib-resources leave the lock file. black, pylint and mypy skip the vendored code, and the pytest filter for Eel's warnings is removed. A new test imports electionguard_gui in a fresh interpreter with DeprecationWarning as an error; it fails with Eel 0.18.2 and passes with the vendored copy. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
A new Linux Check job runs the full test suite on Python 3.12, 3.13, 3.14 and 3.15, and checks that the interpreter is the requested version. The existing Linux Check, MacOS Check and Code Analysis jobs keep their names because they are required checks, and stay on 3.11, the minimum version. pyproject.toml lists 3.11 to 3.15 as classifiers. Co-authored-by: GaziFe <233933832+GaziFe@users.noreply.github.com> Co-authored-by: Abdullah Kadir Çelik <207906693+Kadircelik-stack@users.noreply.github.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Python 3.9 and 3.10 are end-of-life, and several of the latest releases require 3.11 (ipython 9, ipykernel 7, pymongo 4.19, hypothesis).
requires-pythonis now>=3.11,<4.0.0; CI workflows, black/mypy targets and the README/docs are updated to match.Commit 1: dependency upgrade
uv_build0.13.uv.lockregenerated.pytest-mockandtypes-python-dateutilmoved from runtime dependencies to the dev group (only tests and mypy use them).import-selfandcyclic-importdisabled: pylint 4.1 reports false positives on the mkinit-generated__init__.pyfiles when a submodule name starts with the package name (e.g.encrypt_ballots/encrypt_ballots_*). Noted inpyproject.toml; the trade-off is that real import cycles are no longer reported until this is fixed upstream in pylint.Commit 2: refactor
list[...],X | None),collections.abcimports,datetime.UTC.__init__.pyre-exports are untouched.Singleton: all subclasses shared one instance slot, soget_instance()returned the baseSingletonobject rather than the subclass. It now keeps one instance per class and is typed as returningSelf. Added a regression test that fails on the old code.ifs, boolean returns anddict.keys()checks.broad-exception-caughtpylint pragmas onto theexceptline (pylint 4.1 no longer applies the preceding-line form to theexceptclause) and use the current message name.test_utc_to_str_with_valid_utc_dateno longer depends on the local timezone (it failed in UTC+3, where the hour has two digits).Commit 3: GitHub Actions
actions/checkout,actions/setup-python→ v7,actions/upload-artifact→ v7,github/codeql-action→ v4,nick-fields/retry→ v4,astral-sh/setup-uv→ v10.3.0 (still pinned by SHA). The previouscodeql-action@v2andupload-artifact@v2have been retired by GitHub, so CI could not run with them.actions/create-release+actions/upload-release-assetpair is replaced bysoftprops/action-gh-release@v3. The release is now created after the PyPI upload, together with its zip asset.Commits 4–5: design pattern fixes
An audit of the library's patterns (Singleton, Context Manager, Repository, Mediator, Builder, CLI step pipeline, GUI dependency injection) found these defects. Each fix comes with a test that fails on the old code:
DiscreteLog(Singleton): call sites created a throwaway instance withDiscreteLog(), soset_max_exponent/set_lazy_evaluationnever took effect. The library now usesDiscreteLog.get_instance().precompute_cache_asyncfilled a module-level dict instead of the instance cache; it now fills the instance cache.Scheduler(Context Manager):with Scheduler()opened the pools twice and leaked the first set, andtally.py/decryption.pycreated a scheduler per call (per contest during decryption) without ever closing it. Addeduse_scheduler(), which reuses a caller's scheduler or closes the one it creates.close()is now idempotent. The docstring no longer claims the class guarantees a single set of pools.ReadOnlyDataStore(Repository view):__eq__called itself (RecursionError),__iter__yielded(key, value)pairs, and__getitem__returnedNoneinstead of raisingKeyError. As a resultin,keys(),items()anddict(view)were all wrong. It now follows theMappingcontract.Union[...]type aliases now use|syntax.Verification
pytest tests: 252 passed, 0 skipped on both Python 3.11.17 and 3.13 (Windows). Before these changes: 221 passed, 1 failed (the timezone test).uv build,twine checkandmkdocs build --strictpass; workflow YAML validated.Commits 6–20: fixes found by end-to-end testing, CI and review, plus dependency refreshes
Installing the branch into a clean environment and running real elections through the
egCLI found four more defects, and the CI run on this PR found a fifth. All five also exist onmain. Every fix except thehypothesisdependency (a packaging change, checked by a clean install) comes with a test that fails on the old code:egdid not start afterpip install electionguard: the CLI importselectionguard_tools, whose factories importhypothesis, which was only a dev dependency (ModuleNotFoundError).hypothesisis now a runtime dependency.import-ballotswith fewer guardian keys than the guardian count crashed withAssertionError: Unwrap called on None. It now announces missing guardians and has the present ones compensate, using the guardian keys exchanged during the key ceremony. Below the quorum it fails with a clear message.encrypt.pyandelgamal.pylogged every ballot, selection and ciphertext at INFO, which the default console handler prints. Encrypting the six sample ballots printed 288 of the 380 output lines (about 280 KB of hex and raw bytes). The traces only contain public values, so this was noise rather than a leak; they now log at DEBUG.negate_q(0)overflowed: it returnedqinstead of0, so encryption raisedOverflowErrorwhenever the random nonce in the disjunctive Chaum-Pedersen proof was 0. This is practically unreachable with the productionq, but the test constants useq = 65521, so property tests failed at random (theFlakyFailurein Linux Check on CI run #1682). The result is now reduced modq.Two commits refresh dependencies released since the PR was opened, including
python-dateutil4.0.0 (a major version; the code only usesdateutil.parser.parse) andpydantic2.14.3.One commit replaces
test_encrypt_contest_overvote_fails, skipped since 2022 as "runs forever". It no longer hangs, but it expectedencrypt_contestto returnNonefor an overvote, which is not what the code does now: the contest is encrypted with no votes counted and the overvote is recorded in the encrypted contest data. The new test checks that behaviour, including the proofs, and fails if the overvote handling is removed. The suite now has no skipped tests.The last seven commits come from looking for hangs, reviewing that work and repeating the test suite. All of the defects below also exist on
main:rand_range_qcould hang or return a constant: a start at or aboveQlooped forever, a start just below the 256-bitQneeded about 2^256 draws, and a start of 0 always returned 0. It no longer loops. The only caller in the library passes 2, so none of this is reachable from the library today.discrete_logon a value with no small exponent: a wrong key or corrupted data made it search up to the 100 million limit, storing every step (estimated from a 300,000-step run at about 42 minutes and 214 GB). A baby-step giant-step check now rejects these values in under a second (measured 0.37 s, 22 MB) and leaves the cache untouched.decrypt_selection_with_decryption_sharesreturnsNonefor them, as its docstring says. A value more than 100,000 steps past the cache now stores only its own entry, sog^100,000,000takes about 0.14 s instead of storing every step. One known edge case: after such a value is cached, turning off lazy evaluation and precomputing can leave smaller values uncached, which raisesDiscreteLogNotFoundErrorrather than giving a wrong result.Noncescould derive 0 whileelgamal_encryptrequires[1,Q), so encryption returnedNone. With the 16-bit testQthis is about 1 in 65,521 draws and made tests fail at random insetUp. A zero is now replaced by a deterministic re-hash. With the productionQa zero is practically never hashed, so existing nonces and records do not change.DeadlineExceededon a slow machine even though their results were correct. The deadlines are removed; CI keeps its overall timeout.get_contest_description_well_formedcould repeat a candidate id, producing an invalid description. Ids are now unique.eelwarnings: fixed in commit 22, see below.Commits 21–23: Python 3.15, Eel and test deadlines
pyyaml6.0.3 has no Python 3.15 wheel yet, souv sync --all-groupsthere needs a C compiler orPYYAML_FORCE_LIBYAML=0. Onlymkdocs(dev group) uses it, so installing the package is not affected.nestedExprandexcludeCharswhen imported, and its repository is archived. It is copied intosrc/electionguard_eel(MIT, license included) with those two names fixed, imports renamed, stdlibimportlib.resourcesinstead of the backport, and no PyInstaller command line. The GUI imports it aseel, so the GUI code is otherwise unchanged.eel,futureandimportlib-resourcesleave the lock file, and the pytest filter is removed. A test imports the GUI withDeprecationWarningas an error. The trade-off is that the project now maintains about 1,000 lines of third-party code, which black, pylint and mypy skip.DeadlineExceededon Python 3.15.2 on a busy machine although its result was correct. A profile intests/conftest.pynow setsdeadline=Nonefor the session, and a test checks it.End-to-end verification on Windows
mainran the sameeg e2eelection with identical tallies.setup→mark-ballots→encrypt-ballots→submit-ballots→import-ballotswith 5 guardians / quorum 3 on all five manifests indata/(including Hamilton County, 68 selections), decrypted with all 5 guardians and with 2 missing: 10/10 tallies match a hand count of the plaintext ballots, and every record passes an independent check of ballot proofs, aggregation, decryption-share proofs, compensated-share recovery keys and Lagrange reconstruction. A deliberately tampered record is rejected.7e41533: 252 passed, 0 skipped in 8 of 8 runs on Python 3.13 and 2 of 2 on Python 3.11 (91% coverage). At6359d8a: 254 passed, 0 skipped, no warnings, on Windows with Python 3.11.17, 3.12.10, 3.13.17, 3.14.10 and 3.15.2. black, mypy and pylint are clean.test_reconstruct_decryption_sharefailure was not captured; it uses the samesetUpas the zero-nonce failure, so that is the likely cause but it is not confirmed.cyclic-importcheck disabled, a one-off scan found no load-time cycles across 149 modules. It does not cover__init__.pyre-exports or imports inside functions.eguigraphical interface, one full election on Windows with a local MongoDB 7.0 and no Docker, in a browser, with one admin and two guardian instances: key ceremony (2 guardians, quorum 2), create election, upload 8 ballots made witheg(6 cast, 2 spoiled), tally and decrypt with both guardians. The tally page and a spoiled ballot page show the correct values, the tally matches a hand count, and the exported record passes the independent check on both this branch andmain. No errors in the logs.eguirun: thedocker composesetup, the USB ballot upload wizard (the legacy upload form was used), a guardian missing at decryption, and the native file picker (files were set on the inputs by script). The encryption package and record exports were called through the app'seelfunctions, not their pages' buttons.pip installof the wheel runseg e2ewithout installingeel. Theeguielection above, repeated with the vendored Eel, gives the same tally. All 12 records made on 3.15.2 pass the check with a clean install ofmain, and a record made bymainpasses on 3.15.2.src/electionguard_gui/Dockerfilestill installs Python 3.14. It is unchanged, and Docker is not tested.CI
This workflow passed on my fork at
6359d8a, including the new Linux jobs for Python 3.12 to 3.15, without waiting for workflow approval here: https://github.com/ebubekirrcihangirr6060/electionguard-python/actions/runs/43064855041 (the MacOS Check had to be re-run because GitHub found no macOS runner the first time).