Replace licensecheck with scancode-toolkit for dependency license checks - #70
Open
w-martin wants to merge 2 commits into
Open
Replace licensecheck with scancode-toolkit for dependency license checks#70w-martin wants to merge 2 commits into
w-martin wants to merge 2 commits into
Conversation
licensecheck only reads declared PyPI metadata (the License field and trove classifiers), which is self-reported and often absent, stale, or too vague to act on. scancode-toolkit matches the actual license *text* a package ships against a database of ~2500 known licenses, the same way GitHub's own license detector works. It also removes a dependency pin problem: licensecheck's Python 3.11-compatible release (2025.1.0) pulls fhconfparser, which pins attrs<24. The version that fixes it (2026.0.8) requires Python >=3.12, above this project's 3.11 floor. The check now runs over every distribution installed in the dev environment, via scripts/check_licenses.py. Per distribution it text-matches the license files bundled in .dist-info, falling back to declared metadata (resolved through scancode's SPDX symbol table, then its text matcher) only when a package bundles no license file at all. A package passes when at least one detected license is Permissive, Public Domain or Copyleft Limited -- the same policy licensecheck applied for an MIT project via its compatibility matrix. The policy requires a positive allowed match rather than the absence of a disallowed one because packages routinely concatenate their vendored dependencies' license texts into their own LICENSE file; pandas ships BSD-3-Clause alongside notices containing GPL text. A package offering only a disallowed license, or none at all, still fails. scancode-toolkit-mini is used rather than the full distribution: it omits the prebuilt binary plugins, which have no arm64 wheels (extractcode-7z blocks uv resolution on Apple Silicon outright), and only license detection is needed here. libmagic is now a local prerequisite on Apple Silicon, since scancode's bundled typecode-libmagic ships a 1 KB placeholder for macosx_11_0_arm64; DEVELOPING.md documents this. CI does not run this check, so CI is unaffected. Claude-Session: https://claude.ai/code/session_013nRj6HvRbwkzAoj3QPbiSj
CI never invoked verify-licences even under the old licensecheck setup. Install libmagic1 (required for scancode's text-based detection) and run the check as part of lint-and-test.
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.
Replaces
licensecheckwithscancode-toolkitfor theverify-licencestask.Why
Two reasons:
licensecheckonly reads declared PyPI metadata — theLicensefield and trove classifiers — which is self-reported and frequently absent, stale, or too vague to act on. scancode matches the actual license text a package ships against a database of ~2500 known licenses, the same approach GitHub's own license detector takes.licensecheck's Python 3.11-compatible release (2025.1.0) pulls infhconfparser, which pinsattrs<24. The release that fixes it (2026.0.8) requires Python>=3.12, above this project's 3.11 floor.What changed
licensecheckand[tool.licensecheck]removed;scancode-toolkit-mini>=32.5.0added to the dev group.scripts/check_licenses.pyholds the policy;tasks.py'sverify_licencescalls it. Still a singleuv run inv verify-licences, still part ofuv run inv all.scripts/*added to the coverageomitlist, alongside the existingtasks.py/benchmarks/*/examples/*entries. No new lint suppressions —scripts/__init__.pyhandlesINP001, and printing stays intasks.py, which already ignoresT201.DEVELOPING.mdupdated (task usage, the policy, and the libmagic prerequisite).How the check works
The check now runs over every distribution installed in the dev environment (136 packages), not just the 25 direct dev dependencies.
licensecheckalso resolved transitively — that's why its ignore list containedmypy-extensions, a transitive dep of mypy.Per distribution:
.dist-info(LICENSE,COPYING,NOTICE, …).License-Expression/License/License ::classifiers), resolved via scancode's SPDX symbol table and then its text matcher.A package passes when at least one detected license is in an allowed scancode category:
Permissive,Public Domain, orCopyleft Limited(LGPL/MPL/EPL). This mirrors licensecheck's effective policy here — I read itslicense_matrix/matrix.csvMIT row rather than assuming: for an MIT project it passed public domain, permissive, LGPL, MPL and EPL, and failed GPL, AGPL, EUPL, proprietary, unknown and no-license.Why "at least one allowed" rather than "no disallowed": packages routinely concatenate the license texts of their vendored dependencies into their own LICENSE file. pandas's bundled
LICENSEtext-matchesgpl-1.0-plusamong a dozen others, because it ships the notices of everything it vendors — but the grant pandas makes to us is BSD-3-Clause. Failing on any disallowed match would reject pandas, which is plainly wrong. Requiring a positive allowed match still fails a package that offers only a disallowed license, or none at all. This is the one genuine behavioural trade-off of moving from metadata to text detection, and it's documented in both the module docstring and DEVELOPING.md.What I verified
ARM install (the documented blocker is stale, but there's a different one). The claim that
pip install scancode-toolkitfails on Apple Silicon becausepy-ahocorasickandintbitsetlack arm64 wheels is no longer true — both now publishmacosx_11_0_arm64wheels and installed cleanly in a throwaway venv with no build step. The real arm64 problem is elsewhere:typecode-libmagichas never shipped a working arm64 build. Itsmacosx_11_0_arm64wheel is a 1,285-byte placeholder (themanylinux_2_28_aarch64one is 1,912 bytes), and the latest release (5.39.210531, 2021) dropped arm64 entirely. Without a system libmagic, importingscancode.api— or evenlicensedcode— dies withNoMagicLibError.brew install libmagicfixes it; scancode'stypecodeexplicitly globs/opt/homebrew/Cellar/libmagic/*/lib/libmagic.dylib. This is a new local prerequisite and the main thing worth a second opinion — it's documented in DEVELOPING.md's Prerequisites and the Verify Licenses section. CI doesn't run this check (publish.ymlrunsformat-check,lint,testonly), so CI is unaffected.Python version support: scancode-toolkit 32.5.0 declares
requires-python >=3.10and publishes cp310–cp314 wheels, so it covers this project's full 3.11–3.14 range. Verified against PyPI metadata, and exercised on both 3.11 (scratch venv) and 3.14 (the repo's.python-version).-minirather than the full package:uv add scancode-toolkitfails to resolve on Apple Silicon —extractcode-7z==16.5.210531has wheels only formanylinux1_x86_64,macosx_10_14_x86_64andwin_amd64.scancode-toolkit-minidrops the prebuilt binary plugins entirely and requires onlytypecode, which is all that license detection needs.Deliberate-failure test. Temporarily added
pylint(GPL-2.0) to the dev group. The check failed as intended, and detected the GPL from pylint's bundled LICENSE text, not its metadata:EXIT=1.pylintwas then removed andpyproject.toml/uv.lockdiffed byte-identical against their pre-test backups.Full suite:
uv run inv allpasses — 301 Python tests, 185 Rust tests, 100% line/branch coverage threshold, and 136 packages license-checked. (One unrelated flake on the first run:tests/integration/test_regression.pyhitsqlite3.OperationalError: database is lockedon mypy's cache under xdist. It passes in isolation and on rerun; that test has prior history, see #65.)A tool bug worth knowing: scancode's own dependency-manifest route is not usable here.
packagedcode.pypi.PythonInstalledWheelMetadataFile— its dedicated installed-wheel handler — crashes on pandas 3.0.5's METADATA withAttributeError: 'str' object has no attribute 'get'(pypi.py:1663), and it returnsdeclared_license_expression=Nonefor packages using the modernLicense-Expressioncore-metadata field (invoke, maturin, mypy). Hence the hand-rolled scan over.dist-info.Cross-cutting: this may unblock #69
Removing
licensecheckalso removes its transitivefhconfparser→attrs<24pin. That was the constraint blockingpygls>=2for the LSP server in #69.attrsnow resolves to 26.1.0 inuv.lock. I have not touched that branch or its files — flagging it only so it can be picked up separately.Less certain / worth review
brew installand CI is unaffected, but it is genuinely new friction and arguably trades one kind of churn for another.plyadded toIGNORED_PACKAGES. It declares a bare"BSD"— not an SPDX identifier — with no trove classifier and no bundled license file, so nothing in the installed artifact pins down the variant. It's only in the tree because scancode depends on it.licensecheckpassed it via substring matching on"BSD". Upstream ply is BSD-3-Clause.mypy-extensionsandpyreflyboth pass on their own merits under the new checker (both ship a detectable LICENSE), andtypedframeswould pass via the metadata fallback. Happy to trim them if you'd prefer.examples/sql_connectors/*/uv.lockstill referencelicensecheck>=2025.1.0in their recorded copies of typedframes' dev group. Those sub-projects are outside the uv workspace (exclude = ["examples/*/*"]), aren't part ofinv all, and were already stale before this change (they recordcomplexipy>=5.2.0against the root's current>=6.2.0). Regenerating all ten would mean resolving pyspark/databricks/snowflake trees, so I left them — say the word if you'd rather they were refreshed.https://claude.ai/code/session_013nRj6HvRbwkzAoj3QPbiSj