Skip to content

Modernize the package: upgrade all dependencies, fix silent correctness bugs, add tests and CI/CD - #7

Merged
asjacobs92 merged 3 commits into
masterfrom
chore/modernize-dependencies
Aug 24, 2026
Merged

Modernize the package: upgrade all dependencies, fix silent correctness bugs, add tests and CI/CD#7
asjacobs92 merged 3 commits into
masterfrom
chore/modernize-dependencies

Conversation

@asjacobs92

Copy link
Copy Markdown
Collaborator

Summary

Trustee's dependencies dated from 2022 and no longer resolved against current releases. This upgrades the whole stack to the latest of everything, fixes the breakages that exposed, and adds the test suite and CI/CD that would have caught them.

The headline finding: the package imported and ran fine on modern scikit-learn but produced wrong numbers. Nothing crashed — the reports just printed nonsense.

Silent correctness bugs

scikit-learn 1.3 changed classifier tree_.value from raw sample counts to per-node class fractions. Every percentage derived from it was wrong:

Before: virginica: 200.00% / 0.00%    Class Samples: 1400.00%    Data Split: 7.14% (every node)
After:  virginica: 0.00% / 100.00%    Class Samples: 100.00%     Data Split: 35.71% / 64.29%

Fixed with a version-agnostic get_node_counts() helper. Five more real bugs surfaced along the way:

Bug Impact
TrustReport(X=..., y=...) raised AttributeError The documented entry point, so the shipped trust_classification.py example could not run
Leaf prob tested .ndim > 1 on a 1-D array Every probability rendered as 0.00%
Stray trailing comma Class names rendered as (np.str_('virginica'),)
TrustReport.__getstate__ used a shallow copy Detached trustee.expert from the live object; first save() corrupted the report, second raised
plt.figure() immediately superseded by plt.subplots() Leaked a figure per call in four plot helpers

Plus two pandas 3.0 breaks: df.drop(columns=..., axis=1) now raises, and get_dummies returning bool forced the output array to object dtype, silently making the np.nan_to_num scrub a no-op.

Dependency audit

The manifest was wrong in both directions:

  • six and joblib were imported but never declared — a latent ImportError. six is a Python-2 shim and is now gone.
  • scipy, termcolor, setuptools and all four Sphinx packages were declared as runtime deps but unused. pip install trustee was pulling in the whole of Sphinx.

Locked: numpy 2.5.2 · pandas 3.0.5 · scikit-learn 1.9.0 · matplotlib 3.11.1 · prettytable 3.18.0 · graphviz 0.21 · Sphinx 9.1 · furo 2025.12.19

pyproject.toml moved to PEP 621 with poetry-core (the old poetry.masonry.api backend and dev-dependencies are both removed in Poetry 2.x). Python floor 3.73.11. Version 1.1.61.2.0.

One gotcha worth recording: Poetry 2.1 silently does not lock PEP 735 [dependency-groups]. The docs/dev deps were missing from poetry.lock entirely until I switched to Poetry's native group syntax.

Tests and CI/CD

The repo had no tests, and CI only built docs — which is why the rot went unnoticed.

67 tests, ~15s. The tree tests assert on magnitudes, not shapes, because the regression they guard leaves numbers finite and plausible-looking. Verified against the pre-fix code: 6 of them fail.

  • test.yml — lint (black, flake8, poetry check --lock); tests on Python 3.11–3.14 across Linux, macOS and Windows; the shipped examples (they render into the docs, so breakage there breaks the published site); and a build job that runs twine check and verifies the wheel imports standalone.
  • release.yml — builds, re-runs tests, refuses to publish if tag / pyproject.toml / _version.py disagree, then publishes via PyPI Trusted Publishing. TestPyPI available via workflow_dispatch.
  • sphinx.yml — replaced the third-party action pinned to @master (it supplies its own Python and would not pick up the pinned Sphinx 9 stack) with a direct build. Warnings are now errors; deploys gated to master so PRs no longer publish.

pytest now fails on DeprecationWarning/FutureWarning, so the next round of dependency drift surfaces as a test failure rather than silently wrong numbers.

⚠️ Before merging

release.yml needs a one-time PyPI trusted-publisher setup or the release will fail. On PyPI → Manage → Publishing, add: owner TrusteeML, repo trustee, workflow release.yml, environment pypi. Same for TestPyPI with environment testpypi. (Alternative: swap in a PYPI_API_TOKEN secret.)

Also worth noting GitHub currently flags 174 vulnerabilities on master (9 critical, 75 high) — this upgrade should clear the bulk of them.

Verification

All green locally: 67 tests on Python 3.12 and 3.14 · all four shipped examples · clean-venv wheel and sdist installs · Sphinx 9 docs build with -W and zero warnings · black --check and flake8 clean · poetry check --lock clean.

Two things deliberately left alone as pre-existing and out of scope: cosmetic matplotlib findfont/tight_layout warnings, and a benign sklearn UserWarning in the regression report path (allowlisted in the pytest config rather than silenced globally).

🤖 Generated with Claude Code

asjacobs92 and others added 3 commits August 24, 2026 16:26
Trustee's dependency set dated from 2022 and no longer resolved against
current releases. Upgrade the whole stack and fix the breakages it exposed.

Correctness fixes (silent, no crash):

* scikit-learn 1.3 changed `tree_.value` for classifiers from raw sample
  counts to per-node class fractions. Every percentage derived from it was
  wrong -- reports showed class shares of 200% and 1400%, and "Data Split %"
  was pinned at a constant for every node. Add `get_node_counts()` to
  de-normalize across versions and route `get_dt_info()` plus the TrustReport
  call sites through it.
* `TrustReport(X=..., y=...)` raised AttributeError on `X_train.shape[1]`.
  This is the documented entry point, so the shipped
  `trust_classification.py` example could not run.
* Leaf `prob` was always 0.00%: the guard tested `.ndim > 1` on a 1-D array.
* A stray trailing comma rendered class names as `(np.str_('virginica'),)`.
* pandas 3.0 rejects `df.drop(columns=..., axis=1)`.
* pandas >= 2 returns bool from `get_dummies`, which forced the returned
  array to `object` dtype and made the `np.nan_to_num` scrub a no-op. Pin
  the dummy dtype to uint8 to restore numeric output.

Dependency audit -- the manifest was wrong in both directions:

* `six` and `joblib` were imported but never declared (latent ImportError).
  `six` is a Python-2 shim and is dropped outright.
* `scipy`, `termcolor`, `setuptools` and all four Sphinx packages were
  declared as runtime dependencies but unused, so `pip install trustee`
  pulled in the whole of Sphinx. Moved to a docs group or removed.

Packaging:

* pyproject.toml converted to PEP 621 with the poetry-core backend; the old
  `poetry.masonry.api` backend and `dev-dependencies` table are both removed
  in Poetry 2.x. Note that PEP 735 `[dependency-groups]` is silently not
  locked by Poetry 2.1, so docs/dev use Poetry's native group syntax.
* Python floor raised 3.7 -> 3.11; verified on 3.12 and 3.14.
* Fix an invalid `\w` escape that warned on import under Python 3.12.

Locked: numpy 2.5.2, pandas 3.0.5, scikit-learn 1.9.0, matplotlib 3.11.1,
prettytable 3.18.0, graphviz 0.21, Sphinx 9.1, furo 2025.12.19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repo had no tests at all and CI only built the docs, which is why the
dependency rot in the previous commit went unnoticed. Add the suite first so
the pipeline has something to run.

Tests (67, ~15s):

* test_tree.py pins the semantics that scikit-learn 1.3 changed. These
  assertions check magnitudes, not shapes -- the regression they guard leaves
  the numbers finite and plausible-looking, so a shape check would miss it.
  Verified against the pre-fix code: 6 of them fail.
* test_trustee.py covers both explainers, the fit/explain contract, the
  accessors and the not-yet-fitted guard.
* test_dataset.py covers the conversion helpers and `read()`, including the
  numeric-dtype guarantee that the pandas dummies change broke.
* test_report.py covers TrustReport construction through both documented
  entry points, rendering, and the save/load round trip. The percentage and
  class-name assertions guard the report bugs fixed in the previous commit.

Two more bugs surfaced while writing them:

* TrustReport.__getstate__ detached `trustee.expert` from the live object
  rather than from the pickled copy -- `__dict__.copy()` is shallow. The first
  save() corrupted the report in memory and the second raised AttributeError.
* Four plot helpers called plt.figure() immediately before plt.subplots(),
  which supersedes it. plt.close() then only closed the second figure, leaking
  the first on every call. The discarded figsize was never applied, so
  dropping the stray calls changes nothing visually.

Pipelines:

* test.yml -- lint (black, flake8, poetry check --lock), tests across
  Python 3.11-3.14 on Linux plus a macOS and Windows spot-check, the shipped
  examples (they are rendered into the docs, so breakage there breaks the
  published site), and a build job that checks metadata with twine and
  verifies the wheel imports standalone.
* release.yml -- builds, re-runs the tests, refuses to publish if the tag,
  pyproject.toml and trustee/_version.py disagree, then publishes via PyPI
  Trusted Publishing. TestPyPI is available through workflow_dispatch.
  Requires a one-time trusted-publisher setup on PyPI.
* sphinx.yml -- replaced the third-party action pinned to @master, which
  supplies its own Python and would not pick up the pinned Sphinx 9 stack,
  with a direct build. Warnings are now errors, and deploys are gated to
  master so pull requests no longer publish.

Lint is green rather than merely configured: fixed trailing whitespace, an
unused pandas import and a bare except, and gave .flake8 the black-compatible
ignore set with documented per-file exceptions. pytest now fails on
DeprecationWarning and FutureWarning, so the next dependency drift surfaces
as a test failure instead of silently wrong numbers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
checkout v5->v7, setup-python v6->v7, upload-artifact v4->v7 and
download-artifact v5->v8. The artifact majors changed how direct
(unzipped) uploads are handled and now error on digest mismatch; both
are opt-in or transparent for the directory upload used here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@asjacobs92 asjacobs92 self-assigned this Aug 24, 2026
@asjacobs92
asjacobs92 merged commit 7bffc1d into master Aug 24, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant