Skip to content

Dependency licenses

pyDEXPI checks the licenses of its dependencies automatically. Every pull request runs the check, so a dependency with an incompatible license is caught in review rather than after release.

This page is for maintainers: it explains the policy, how to run the check locally, and what to do when it fails.


Why the policy is permissive-only

pyDEXPI is released under the AGPL-3.0, but is also offered under a commercial license on request. That second option is what drives the policy.

If pyDEXPI depended on a copyleft library, it could no longer be relicensed for a customer who needs a proprietary build — the dependency's own terms would follow it. So the policy is stricter than "compatible with the AGPL": only permissive licenses are allowed by default.

Allowed without review:

Family Examples
MIT MIT, MIT-CMU
BSD 0BSD, BSD-2-Clause, BSD-3-Clause
Apache Apache-2.0
ISC ISC
Python Python-2.0, PSF-2.0
Public domain CC0-1.0, Unlicense
Other permissive BSL-1.0 (Boost), NCSA, UPL

Not allowed without a reviewed exception: GPL, LGPL, AGPL, MPL, EPL, EUPL, and anything proprietary. Also not allowed: packages whose license metadata is missing or unreadable. An unknown license is not evidence of a permissive one, so it fails until somebody checks it.

Failing the policy is not the same as being legally incompatible

The policy is deliberately stricter than compatibility. Weak-copyleft licenses such as MPL-2.0 and the LGPL permit a larger work to be distributed under other terms, keeping obligations on the covered files or library themselves — they do not normally prevent commercial or proprietary use. They fail this policy because pyDEXPI's default is permissive-only, not because the dependency is problematic.

The report and license-exceptions.toml therefore distinguish a reviewed weak-copyleft dependency from a genuine policy exception. Nothing in this repository is legal advice or legal approval; the exceptions are the maintainers' compatibility assessments.

Where the policy lives

File Contents
pyproject.toml, [tool.licensecheck] The policy: allowed licenses, disallowed licenses, which extras to scan.
license-exceptions.toml Packages exempted after a manual review, with evidence.
scripts/licenses The runner, used identically in CI and locally.
scripts/license_report.py Builds the Markdown report and validates the exceptions file.
.github/workflows/licenses.yml Runs the check on every push to main and every pull request.

The check covers the core dependencies and every extra (png, pdf, export, cli, dev), plus everything those pull in transitively — currently around 70 packages.

Dependency groups and what they mean

The whole tree is checked, but the report records why each package is present, because that changes how seriously a finding should be taken. Groups are resolved with uv, one per extra, and each package is attributed to the first group below that contains it:

Group Reaches users?
core (runtime) Yes — installed by every pip install pyDEXPI
cli extra, png extra, pdf extra, mcp extra Yes — installed by users who opt in, and redistributable by them
docs tooling, test tooling, lint tooling No — only used to build and test pyDEXPI

A copyleft library inside the pdf extra is a real question: users install it and may redistribute it. The same license in the docs toolchain is close to irrelevant, because it never leaves our CI. The report splits failures along exactly that line, so review effort goes where it matters.

This is why the dev extra is also declared as three narrower extras (lint, test, docs). dev remains their union, so pip install .[dev] is unchanged — the split exists so the report can tell documentation tooling apart from test tooling.

Running the check locally

pip install licensecheck uv
./scripts/licenses

uv is required, not optional. licensecheck shells out to it to resolve the dependency tree from pyproject.toml; its other resolvers cannot read a pyproject.toml and fall back to inspecting only already-installed packages, one level deep. That produces a report that looks clean because it is incomplete, so scripts/licenses refuses to run without uv.

The run writes license-report.json (machine-readable, the full inventory) and license-report.md (the summary). Both are gitignored, and CI uploads them as the dependency-license-report artifact — including when the job fails, which is when you most want them. In CI the summary is also written to the workflow's Summary tab, so the failing package is visible without downloading anything.

When the check fails

The report names the offending package, its license, and whether it is direct (declared in our own pyproject.toml) or transitive (pulled in by something else). Start there:

  • Direct dependency. We chose it. Prefer a permissively licensed alternative. If there is no reasonable alternative, weigh the license against the commercial-licensing constraint above before considering an exception.
  • Transitive dependency. Check which of our dependencies pulls it in (uv pip tree or pipdeptree). Sometimes an upstream bump removes it, or the parent offers an extra that avoids it.
  • Unknown or missing metadata. Usually a packaging bug upstream, not a licensing problem. Read the project's actual LICENSE file and record what you find.

Never silence a failure by loosening the allowlist for one package — widening only_licenses allows that license for every dependency, now and in future. Use an exception, which is scoped to the one package and carries a reason.

Do not add ignore_licenses to the policy either. Terms licensecheck does not recognise collapse into its UNKNOWN category, so ignoring one unrecognised license would quietly mark every package with unreadable metadata as compliant.

Adding a reviewed exception

  1. Verify the license at the source. Read the LICENSE file in the project's own repository, not just the PyPI classifier — classifiers are frequently wrong or stale.
  2. Decide whether it is genuinely acceptable, and write down why. Useful questions: is it an optional extra or a core dependency? Is it shipped to users or only used to build and test? Do we modify it? Would a weak-copyleft library's terms actually reach our code?
  3. Add an entry to license-exceptions.toml:
[[exception]]
package = "svglib"
version_spec = ">=2.0.0,<3.0.0"
verified_license = "LGPL-3.0-or-later"
classification = "weak-copyleft"
source = "https://pypi.org/project/svglib/ (License-Expression metadata field)"
reviewed_on = "2026-07-30"
reviewed_by = "pyDEXPI maintainers"
reason = """
Why this is acceptable for pyDEXPI.
"""

package, version_spec, verified_license, classification, source, reviewed_on and reason are all required; the check fails if any is missing. When the report flags an unknown license it prints a ready-made template you can paste.

classification must be either weak-copyleft (file- or library-level copyleft such as MPL or LGPL — reviewed and found not to conflict) or policy-exception (something the policy genuinely disallows, accepted anyway for the stated reason). The second should be rare and should attract close scrutiny.

  1. Get it reviewed. The exception is a licensing decision, so it should be approved by a maintainer in the pull request like any other change.

version_spec is what keeps an exception honest. The check fails if the version CI resolves falls outside the range that was actually reviewed, so a major upgrade forces a fresh look rather than inheriting a verdict from an older release. Keep ranges tight enough that a relicensing upstream cannot slip through — a major-version bound is usually the right call.

If a package no longer needs its exception — it was dropped, or upstream relicensed — the report flags the entry as stale so it can be deleted. That is a warning rather than a failure, so a routine dependency bump does not break CI.

Current exceptions

Three, all classified as reviewed weak-copyleft dependencies rather than policy exceptions, all reviewed on 2026-07-30, with the full reasoning in license-exceptions.toml:

Package License Group Assessment
svglib LGPL-3.0-or-later pdf extra — reaches users Unmodified, separately identifiable dependency. LGPL-3.0 permits distributing a Combined Work under other terms, subject to its requirements for the library itself and the user's ability to replace or modify it.
certifi MPL-2.0 mcp extra — reaches users Not an incompatibility. File-level copyleft, a separate package used unmodified; it arrives through httpx (and, for the docs build, requests).
pathspec MPL-2.0 docs tooling Same as certifi, via mkdocs.

svglib and certifi are the two of the three that reach users. certifi needs no more than the table says: MPL-2.0 binds only its own files, which pyDEXPI never modifies. svglib is the one worth re-reading. Its assessment holds for pyDEXPI as distributed today under the AGPL-3.0. The case that would change the answer is bundling it into a proprietary or frozen executable, where preserving notices, source availability and the user's replacement rights becomes the binding constraint — re-assess before doing that, and re-assess if svglib is ever vendored, patched, or promoted to a core dependency.

Updating the pinned tools

.github/workflows/licenses.yml pins licensecheck and uv on purpose: a checker that changes its verdicts between runs is worse than no checker. When bumping either pin, run ./scripts/licenses locally first and check that the package count and the set of exceptions have not changed unexpectedly.