License Checking¶
zaojun can verify that every dependency's license is compatible with your project's own license. The check is opt-in and uses the license metadata already present in the PyPI JSON response — no extra HTTP requests are needed.
Overview¶
When license checking is enabled, zaojun:
- Reads each dependency's SPDX license expression from PyPI.
- Compares it against an allowed set of SPDX identifiers.
- Flags violations in the output and optionally in the exit code.
- Warns (or fails, or ignores) dependencies whose license cannot be determined.
Enabling license checking¶
Via [tool.zaojun]¶
Via allowed-licenses (auto-enables checking)¶
Setting allowed-licenses automatically enables check-licenses:
Via CLI flag¶
Specifying the allowed set¶
Explicit list¶
Values must be valid SPDX license identifiers
(e.g. "MIT", "Apache-2.0", "LGPL-2.1-or-later"). Non-string entries warn and
are skipped.
Auto-derived from project.license¶
When check-licenses = true but allowed-licenses is not set, zaojun reads
project.license from the same pyproject.toml and derives a safe allowed set
automatically:
The derivation follows these compatibility tiers:
| Project license | Allowed dependency licenses |
|---|---|
| Permissive (MIT, Apache-2.0, BSD, ISC, …) | Permissive only |
| Weak copyleft (LGPL, MPL-2.0, EUPL-1.2) | Permissive + weak copyleft |
| GPL-2.0-only | Permissive (excl. Apache-2.0) + GPL-2.0-only |
| GPL-2.0-or-later, GPL-3.0 | Permissive + weak copyleft + GPL-3.0 family |
| AGPL-3.0 | All of the above + AGPL-3.0 |
GPL-2.0-only and Apache-2.0
Apache-2.0's additional patent termination clause makes it legally incompatible
with GPL-2.0-only. zaojun excludes Apache-2.0 from the auto-derived allowed set
when your project is GPL-2.0-only.
Compound project licenses¶
If your project is dual-licensed (e.g. license = "MIT OR Apache-2.0"), zaojun
derives the allowed set by intersecting the tiers for each component. The reasoning
is that your deps must be compatible with every license option you offer — a dep that
only works under GPL-3.0 would be unusable by users who choose your MIT option.
| Project license | Derived allowed set |
|---|---|
MIT OR Apache-2.0 |
Permissive (both map to the same tier) |
GPL-3.0-only OR MIT |
Permissive only (MIT is the binding constraint) |
MIT AND Apache-2.0 |
Permissive (intersection; same result as OR here) |
If any component of a compound expression is unrecognised or proprietary, no defaults are applied — zaojun behaves as if the project license were unknown.
When the project license is a plain (non-compound) unrecognised identifier, no defaults
are applied and zaojun treats all licenses as unknown (behaviour controlled by
unknown-license).
How license data is resolved¶
For each dependency, zaojun queries the PyPI JSON API and extracts a license in this priority order:
info.license_expression— PEP 639 SPDX expression (most authoritative).info.classifiers— Trove license classifiers mapped to SPDX.info.license— free-text field, normalised to SPDX where possible.None— treated as UNKNOWN.
The resolved SPDX value is stored in the cache so subsequent runs pay no lookup cost.
Compound dependency license expressions¶
Many packages on PyPI are dual-licensed and publish a compound SPDX expression such as
Apache-2.0 OR BSD-2-Clause or MIT OR Apache-2.0. zaojun parses these correctly:
| Operator | Meaning | Passes if… |
|---|---|---|
OR |
User may choose either license | any alternative is in the allowed set |
AND |
All licenses apply simultaneously | all components are in the allowed set |
WITH |
License plus an exception clause | exception is stripped; base ID is checked |
A dep whose expression is Apache-2.0 OR BSD-2-Clause passes for a permissive project
even if only one of the two alternatives is in the auto-derived allowed set — you can
use the compatible alternative.
Handling unknown licenses¶
When a dependency's license cannot be determined, unknown-license controls the
behaviour:
| Value | Effect |
|---|---|
"warn" |
Print a warning; exit code unaffected (default) |
"fail" |
Treat as violation; set exit code 1 |
"allow" |
Silently skip; no output, no exit code effect |
CLI equivalent:
Ignoring specific packages¶
Use license-ignore to exempt a package from license checking. A reason field is
required and must be non-empty — this enforces an audit trail.
[[tool.zaojun.license-ignore]]
package = "some-proprietary-pkg"
reason = "Internal tool — license cleared by legal team on 2025-01-15"
[[tool.zaojun.license-ignore]]
package = "legacy-lib >= 3.0"
reason = "Switched to BSD-2-Clause in v3.0; earlier versions had a custom license"
The package value is a standard PEP 508 specifier (name or name op version).
When a version specifier is present, the ignore rule only applies when the PyPI-latest
version satisfies it — the rule automatically becomes active again if the package is
updated beyond the range.
Entries with missing or empty reason, invalid package specifiers, or non-dict
shapes emit a warning and are skipped.
Output¶
License violation line¶
Dependencies with a license violation gain an inline ⚖️ indicator:
License Violations block¶
After the main report, a dedicated block lists all violations:
License Violations
─────────────────
requests: GPL-3.0-only is not in the allowed set
mypkg: license could not be determined
The block is suppressed by --short, but the ⚖️ inline indicator is always shown.
Header (verbose mode)¶
When --short is not set, zaojun prints the active license check context before
listing dependencies. This makes the defaults visible without reading the config:
License check: MIT (auto-derived)
Allowed: 0BSD, Apache-2.0, BSD-2-Clause, BSD-3-Clause, CC0-1.0, ISC, MIT, ...
Or for an explicit list:
JSON output¶
When --format json is used, each dependency record gains license and
license_status fields:
{
"name": "requests",
"spec": ">=2.28.0",
"status": "up_to_date",
"license": "Apache-2.0",
"license_status": "violation"
}
license_status is "ok", "violation", or "unknown".
Configuration reference¶
| TOML key | CLI | Type | Default | Notes |
|---|---|---|---|---|
check-licenses |
--check-licenses |
boolean | false |
Auto-enabled when allowed-licenses is set |
allowed-licenses |
(config only) | list of SPDX strings | [] |
Empty → auto-derive from project.license |
unknown-license |
--unknown-license |
string | "warn" |
"warn" / "fail" / "allow" |
license-ignore |
(config only) | list of dicts | [] |
Each dict needs package and reason |
Example configuration¶
[tool.zaojun]
check-licenses = true
unknown-license = "warn"
# Optional: explicit allowed list overrides auto-derive
allowed-licenses = [
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"0BSD",
]
# Exemptions with mandatory audit trail
[[tool.zaojun.license-ignore]]
package = "some-pkg"
reason = "Manually reviewed — BSD-compatible despite missing SPDX field"
[[tool.zaojun.license-ignore]]
package = "legacy-tool >= 4.0"
reason = "Re-licensed to MIT in v4.0; earlier versions used a custom license"
[project]
license = "MIT"