Skip to content

CLI

The trustedlicenses command (built with Typer) checks installed packages against the policy declared in pyproject.toml or trustedlicenses.toml. It has two modes: check the current environment (the default), or check a candidate package before adding it (check).

Basic usage

trustedlicenses

With no policy configured yet, behavior depends on whether a real terminal is attached: interactively, it runs a guided setup wizard that explains each license category and writes the config for you; non-interactively (or with --quiet), it reports every detected license without judgment instead of prompting or erroring. See Usage Guide § No policy configured yet.

Checking a candidate package

trustedlicenses check <package> [<package> ...]

Resolves each <package> (a pip-style requirement, e.g. requests or "django>=5,<6") and its full transitive dependency tree into an isolated temporary location, then evaluates the whole set against your project's policy — without installing anything into your real environment. Same wizard-vs-report-mode rule as the environment check applies when no policy is configured yet. See Usage Guide § Checking a package before you add it.

Flags

  • --pyproject PATH (default ./pyproject.toml) — path to the pyproject.toml holding [tool.trustedlicenses] (a sibling trustedlicenses.toml, if present, takes priority — see Usage Guide § Step 1). Applies to both modes; goes before the check subcommand if used:

shell trustedlicenses --pyproject path/to/pyproject.toml trustedlicenses --pyproject path/to/pyproject.toml check requests

  • --quiet / -q — never prompt interactively; fall back to report-only mode (environment check) or a hard error (check) when no policy is configured, even if a real terminal happens to be attached. Use this in CI/CD and pre-commit hooks — see Usage Guide § No policy configured yet for a sample pre-commit hook entry. As a second safety net if this gets left off, every wizard prompt also individually times out (wizard.PROMPT_TIMEOUT_SECONDS, 30s) and aborts the same way declining it does — see Usage Guide § No policy configured yet for why that matters even with --quiet in place.

Exit codes

Code Meaning
0 Every checked package had an allowed license, or no policy is configured yet and report-only mode ran (environment check only)
1 At least one package failed the policy; a config file exists but is misconfigured; check was run with no policy configured (and non-interactively, or the wizard was declined); the candidate package(s) couldn't be resolved (check only); or the wizard was declined during an interactive environment check

See Usage Guide for the full config format and output examples.


trustedlicenses.cli.main_command(ctx, pyproject=typer.Option(Path('pyproject.toml'), '--pyproject', help='Path to the pyproject.toml holding [tool.trustedlicenses].'), quiet=typer.Option(False, '--quiet', '-q', help='Never prompt interactively; fall back to report-only mode when no policy is configured. Recommended for CI/CD and pre-commit hooks.'))

Check installed dependency licenses against policy.

Source code in src/trustedlicenses/cli.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
@app.callback(invoke_without_command=True)
def main_command(
    ctx: typer.Context,
    pyproject: Path = typer.Option(
        Path("pyproject.toml"), "--pyproject", help="Path to the pyproject.toml holding [tool.trustedlicenses]."
    ),
    quiet: bool = typer.Option(
        False,
        "--quiet",
        "-q",
        help=(
            "Never prompt interactively; fall back to report-only mode when no policy is configured. "
            "Recommended for CI/CD and pre-commit hooks."
        ),
    ),
) -> None:
    """Check installed dependency licenses against policy."""
    ctx.obj = {"pyproject": pyproject, "quiet": quiet}
    if ctx.invoked_subcommand is None:
        raise typer.Exit(code=_check_environment(pyproject, quiet=quiet))

trustedlicenses.cli.check(ctx, packages=typer.Argument(..., help="Package requirement(s) to check, e.g. requests 'django>=5,<6'"))

Check whether package(s) could be added without a license problem, before adding them.

Source code in src/trustedlicenses/cli.py
232
233
234
235
236
237
238
@app.command()
def check(
    ctx: typer.Context,
    packages: list[str] = typer.Argument(..., help="Package requirement(s) to check, e.g. requests 'django>=5,<6'"),
) -> None:
    """Check whether package(s) could be added without a license problem, before adding them."""
    raise typer.Exit(code=_check_new_packages(ctx.obj["pyproject"], packages, quiet=ctx.obj["quiet"]))

trustedlicenses.wizard.run(pyproject_path)

Run the interactive setup wizard, writing a policy file.

Parameters:

Name Type Description Default
pyproject_path Path

Path to the project's pyproject.toml -- used to derive where a standalone trustedlicenses.toml would live, and as the file edited in place if the user chooses that instead.

required

Returns:

Type Description
Path | None

The path written, or None if nothing was written -- guided setup was

Path | None

declined up front, no categories were selected, the final confirmation was

Path | None

declined, or no answer arrived within PROMPT_TIMEOUT_SECONDS for some

Path | None

prompt (all treated the same as a declined setup, not a failure: see

Path | None

func:trustedlicenses.cli.main_command's report-only fallback).

Source code in src/trustedlicenses/wizard.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def run(pyproject_path: Path) -> Path | None:
    """Run the interactive setup wizard, writing a policy file.

    Args:
        pyproject_path: Path to the project's ``pyproject.toml`` -- used to derive
            where a standalone ``trustedlicenses.toml`` would live, and as the file
            edited in place if the user chooses that instead.

    Returns:
        The path written, or ``None`` if nothing was written -- guided setup was
        declined up front, no categories were selected, the final confirmation was
        declined, or no answer arrived within ``PROMPT_TIMEOUT_SECONDS`` for some
        prompt (all treated the same as a declined setup, not a failure: see
        :func:`trustedlicenses.cli.main_command`'s report-only fallback).
    """
    try:
        return _run(pyproject_path)
    except _PromptTimeoutError:
        typer.secho(
            f"\nNo answer received within {PROMPT_TIMEOUT_SECONDS:.0f}s -- aborting setup. "
            "(Running non-interactively? Pass --quiet to skip this wizard entirely.)",
            fg="yellow",
            bold=True,
        )
        return None