Metadata-Version: 2.4
Name: trustedlicenses
Version: 0.3.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Dist: typer>=0.27.2
Requires-Dist: tomlkit>=0.15.1
Requires-Dist: packaging>=24.2
License-File: LICENSE
License-File: NOTICE
Summary: Policy-driven license auditing for installed Python dependencies, preferring declared package metadata and falling back to license-text matching.
Keywords: license,compliance,license-check,spdx,dependencies,audit
Home-Page: https://github.com/w-martin/trustedlicenses
Author: William Martin
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/w-martin/trustedlicenses
Project-URL: Issue Tracker, https://github.com/w-martin/trustedlicenses/issues
Project-URL: Repository, https://github.com/w-martin/trustedlicenses

# trustedlicenses

[![CI](https://github.com/w-martin/trustedlicenses/actions/workflows/ci.yml/badge.svg)](https://github.com/w-martin/trustedlicenses/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/pypi/v/trustedlicenses.svg)](https://pypi.org/project/trustedlicenses/)
[![Python versions](https://img.shields.io/pypi/pyversions/trustedlicenses.svg)](https://pypi.org/project/trustedlicenses/)
[![Documentation](https://readthedocs.org/projects/trustedlicenses/badge/?version=latest)](https://trustedlicenses.readthedocs.io/en/latest/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

> ⚠️ **Project Status: Experimental**
>
> `trustedlicenses` (v0.3.0) is an early, experimental release. Detection, policy
> evaluation, and the CLI work end-to-end with full test coverage, but the API and
> config format aren't stable yet, and license detection — declared-metadata parsing
> and the Rust text-matching fallback alike — can be wrong. See the
> [legal disclaimer](#legal-disclaimer) before relying on this for a real compliance
> decision.

**`trustedlicenses` checks that every package installed in your Python project has a
license you've agreed to allow, so you can gate a CI build on it.** Point it at your
environment, tell it which kinds of license are acceptable, and it tells you which
packages don't comply and why.

## Quick look

Nothing to configure first — just run it. With no policy set up yet, a real terminal
walks you through one interactively, explaining what each license category actually
means as you go:

```shell
$ trustedlicenses
Detected licenses for 20 of 20 installed packages.

No policy configured yet -- would you like to run the guided setup? [Y/n]: y

Permissive: MIT, BSD, Apache-2.0, ISC, ... -- minimal restrictions: use, modify, and
redistribute freely, usually with just an attribution/copyright notice.
  (20 detected)
Allow Permissive licenses? [Y/n]: y
...
Wrote policy to pyproject.toml.
```

That writes a `[tool.trustedlicenses]` table to your `pyproject.toml` (or a
standalone `trustedlicenses.toml`, your choice) — see
[No policy configured yet?](#no-policy-configured-yet) below for the full transcript.
Or skip the wizard and write it yourself:

```toml
[tool.trustedlicenses]
allowed-categories = ["Permissive", "Public Domain", "Copyleft Limited"]
```

Either way, running it again just checks:

```shell
uv run trustedlicenses
```

If everything's fine, you get a one-line pass:

```
Checking dependency licenses...
✓ All 134 packages passed.
```

If something isn't, you get exactly what's wrong, and a concrete suggestion for how
to fix it:

```
Checking dependency licenses...
✗ Disallowed or undetectable licenses in 2 of 134 packages:
  certifi: detected MPL-2.0 (categories: Copyleft Limited) -- from declared metadata
    -> add "Copyleft Limited" to allowed-categories, or "certifi" to ignored-packages, to allow this
  fqdn: detected MPL-2.0 (categories: Copyleft Limited) -- from license files: LICENSE
    -> add "Copyleft Limited" to allowed-categories, or "fqdn" to ignored-packages, to allow this
```

The process exits non-zero on any failure, so it plugs straight into CI. `✓`/`✗`
lines are green/red (and compatibility notes yellow) in a real terminal — colors are
stripped automatically when output isn't a TTY (piped, redirected, `NO_COLOR`),
exactly like ANSI color handling in most CLI tools.

Full docs: **[trustedlicenses.readthedocs.io](https://trustedlicenses.readthedocs.io/)**

## Installation

```shell
uv add --dev trustedlicenses
```

## Usage

Add a `[tool.trustedlicenses]` table to your `pyproject.toml` (or run `trustedlicenses`
interactively and let the wizard write it — see below):

```toml
[tool.trustedlicenses]
allowed-categories = ["Permissive", "Public Domain", "Copyleft Limited"]
ignored-packages = ["mypy-extensions"]
```

A standalone `trustedlicenses.toml` (same keys, no `[tool.trustedlicenses]` wrapper)
works too, and takes priority if both exist.

- **`allowed-categories`** (required, no default) — the kinds of license your
  project accepts. A package passes if at least one of its detected licenses falls
  into one of these categories. There's no default on purpose: you say what you're
  willing to accept, rather than inherit an assumption.
- **`ignored-packages`** (optional) — specific packages to skip entirely, for cases
  you've reviewed by hand and decided are fine regardless of what's detected.

If your own project declares its license (`[project.license]`, per
[PEP 639](https://peps.python.org/pep-0639/)), `trustedlicenses` also checks it against
each dependency for a small number of specific, well-documented copyleft
compatibility problems — e.g. a real installed environment where a GPL-2.0-only
project pulled in `scipy` (GPL-3.0-or-later):

```
i 1 compatibility note(s) -- not a pass/fail result, see below:
  scipy: your project is GPL-2.0-only; scipy is GPL-3.0-or-later -- the FSF states
  GPLv2 is not, by itself, compatible with GPLv3 (https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility)
```

This is deliberately narrow and never affects pass/fail — see
[Usage Guide § compatibility notes](https://trustedlicenses.readthedocs.io/en/latest/usage/#a-narrow-fsf-grounded-compatibility-check)
for exactly what it does and doesn't check, and why.

Then run:

```shell
uv run trustedlicenses
```

This checks every package installed in the current environment. See the
[Usage Guide](https://trustedlicenses.readthedocs.io/en/latest/usage/) for the full
category vocabulary, embedding the check in your own code, and how detection works
under the hood.

### No policy configured yet?

**In a real terminal**, running `trustedlicenses` with nothing configured first
reports how many installed packages actually have a detectable license, then offers
the interactive wizard shown above — allow/decline each of Permissive, Public
Domain, Copyleft Limited, and (strong) Copyleft with an explanation for each *and*
how many (and, for one or two, which) of your installed packages fall into it, choose
`pyproject.toml` or a standalone `trustedlicenses.toml`, and it writes the config and
runs the check immediately.

**Without a real terminal — CI, pre-commit, piped input, or `--quiet` explicitly —**
it never prompts (that would just hang a pipeline). Instead: report-only mode, every
installed package's detected license and category, no pass/fail judgment, exit code
`0`:

```
$ trustedlicenses --quiet
i pyproject.toml has no policy configured yet -- showing detected licenses only.
  babel: BSD-3-Clause (Permissive)
  certifi: MPL-2.0 (Copyleft Limited)
  jinja2: BSD-3-Clause (Permissive)
  ...
```

**Use `--quiet` (`-q`) in CI/CD and pre-commit hooks.** Both are non-interactive
already, so `trustedlicenses` falls back on its own — but pass `--quiet` explicitly
so that holds even if a step happens to have a terminal attached.

### pre-commit

`trustedlicenses` audits whatever's actually installed in the current Python
environment, so the hook needs to run with your project's own dependencies already
installed — not in an isolated hook-specific environment the way most pre-commit
hooks work. Add `trustedlicenses` as a dev dependency (see
[Installation](#installation)), then reference this repo directly:

```yaml
- repo: https://github.com/w-martin/trustedlicenses
  rev: v0.3.0
  hooks:
    - id: trustedlicenses
```

Or write the same thing as a local hook without depending on this repo's tag:

```yaml
- repo: local
  hooks:
    - id: trustedlicenses
      name: trustedlicenses
      entry: trustedlicenses --quiet
      language: system
      pass_filenames: false
```

### GitHub Actions

A composite action wraps the same install-then-run steps. Run it in the same job as
your dependency install step, after your project's own dependencies are already on
the Python path:

```yaml
- name: Install dependencies
  run: pip install -r requirements.txt   # or uv sync, poetry install, ...

- name: Check dependency licenses
  uses: w-martin/trustedlicenses@v0.3.0
```

It accepts two optional inputs: `version` (pin the `trustedlicenses` release, as a
pip version specifier — defaults to latest) and `args` (defaults to `--quiet`). It
installs with `uv pip install` when `uv` is already on `PATH`, falling back to plain
`pip install` otherwise.

As a second safety net if `--quiet` gets left off by mistake, every wizard prompt
also times out after 30 seconds with no answer — some CI runners attach something
that looks enough like a real terminal that this can't be told apart reliably, so a
misconfigured job times out and falls back gracefully instead of hanging forever.

An actual misconfiguration (a config with an empty or missing `allowed-categories`)
is always a hard error, with the exact TOML to add — never the wizard, never the
report-only fallback.

### Checking a package before you add it

`trustedlicenses check <package> [<package> ...]` resolves the package(s) — and every
transitive dependency — into an isolated temporary location, and checks the whole set
against your project's policy, without installing anything into your real
environment or assuming which installer (`uv`, `pip`, Poetry, Pipenv, ...) your
project uses. A real example, checking `requests` against a Permissive-only policy:

```
$ trustedlicenses check requests
Resolving requests and its transitive dependencies...
Checking 5 package(s) (requested plus transitive dependencies)...
✗ Disallowed or undetectable licenses in 1 of 5 packages:
  certifi: detected MPL-2.0 (categories: Copyleft Limited) -- from declared metadata
    -> add "Copyleft Limited" to allowed-categories, or "certifi" to ignored-packages, to allow this
```

## Why not just read `pip list`'s license column?

Most Python license tools ([`pip-licenses`](https://github.com/raimon49/pip-licenses),
[`licensecheck`](https://github.com/FHPythonUtils/LicenseCheck)) only read what a
package *says* its license is, in its own metadata. That's usually right, but a
meaningful slice of installed packages declare nothing usable at all — no metadata to
read, so nothing to check.

`trustedlicenses` does that same check first, then — only when a package hasn't
declared anything usable — actually reads the license *text* it ships and matches it
against the official list of known open-source licenses. No extra software to
install, and it doesn't need special system libraries the way some older tools in
this space do. A plain check makes no network calls at all; `trustedlicenses
index-check` is a separate, opt-in command (see the
[Usage Guide](https://trustedlicenses.readthedocs.io/en/latest/usage/#would-upgrading-help-opt-in-uses-the-network))
for the rarer case where nothing usable is declared *or* bundled, and the fix is
upgrading.

See **[Comparison to Alternatives](https://trustedlicenses.readthedocs.io/en/latest/comparison/)**
for how this differs from `pip-licenses`, `licensecheck`, `liccheck`, and ScanCode
Toolkit in practice.

## Speed

On a 425-package real-world environment (data science + web + cloud + ML stack, full
transitive dependency trees — see [Performance](https://trustedlicenses.readthedocs.io/en/latest/performance/)
for exactly what's in it), `trustedlicenses` comes out fastest of the tools tested,
despite doing genuine text-matching work for the ~23% of packages with no usable
declared metadata:

| Tool | Median | Range |
|---|---|---|
| **`trustedlicenses`** | **1.29s** | 1.25s – 2.05s |
| `pip-licenses` | 1.60s | 1.56s – 2.58s |
| `licensecheck` | 2.13s | 2.08s – 9.30s |
| `liccheck` | — | crashes on a current Python/setuptools combination |

That speed isn't free or automatic: without releasing Python's GIL during each Rust
text-matching scan and without running scans in parallel, the same fallback work
takes **7.7–8.9s** on this same environment — roughly 6x slower. Instead, the
fallback scans run concurrently across a thread pool, and the underlying Rust
matcher releases the GIL for the duration of each scan, so independent per-package
work actually runs on multiple cores rather than serializing behind Python's
interpreter lock. See
[Performance](https://trustedlicenses.readthedocs.io/en/latest/performance/) for the
full breakdown, methodology, and why `liccheck` couldn't be measured at all.

## Legal disclaimer

**trustedlicenses is not a lawyer and does not provide legal advice.** Its output —
which license a package resolves to, which category that falls into, and whether a
package passes your configured policy — is a best-effort technical signal, not a
legal opinion. It can be wrong: a package's declared metadata can be inaccurate or
absent, and the text-matching fallback is a similarity match with a real, disclosed
false-negative/false-positive tradeoff (see
[Usage Guide § confidence threshold](https://trustedlicenses.readthedocs.io/en/latest/usage/#a-note-on-the-text-matching-fallbacks-confidence-threshold)
for a concrete case where this matters). Do not rely on `trustedlicenses`'s output as
a substitute for review by a qualified professional before making a legal or license-
compliance decision. Use of this software is entirely at your own risk — see
[LICENSE](LICENSE) for the full disclaimer of warranty.

See also: [en.wikipedia.org/wiki/IANAL](https://en.wikipedia.org/wiki/IANAL).

## Status

Experimental — API and config format are not yet stable.

